Spring Data - Projection and #Query nested property - spring

Assume I have those DTO:
public interface ForumDTO extends ForumBaseDTO{
Integer getId();
ThreadDTO getLastThread();
}
public interface ThreadDTO {
Integer getId();
Integer getCommentCount()
}
In my Repository I have this query using those DTO as projection:
#Query("select forum.id as id, " +
"forum.name as name, " +
"lastThread.id as lastThread_id " +
"from Forum forum " +
"inner join forum.lastThread as lastThread " +
"where forum.parent.id = ?:"
)
Iterable<ForumDTO> findAllByParentId(Integer id);
I can access id,name in ForumDTO using this repo just fine, but with lastThread it just return null. I have tried as lastThread.Id,as lastThread_id, as lastThreadId but none of them work.

You're almost there.
You need to access it from forum to follow out the foreign key:
#Query("select forum.id as id, " +
"forum.name as name, " +
"**forum.lastThread.id** as lastThread_id " +
"from Forum forum " +
"inner join forum.lastThread as lastThread " +
"where forum.parent.id = ?:"
)
Iterable<ForumDTO> findAllByParentId(Integer id);
That said, you're killing yourself with extra work.
The same Query can be written as:
#Query("select forum from Forum where forum.parent.id = :forumId")
Iterable<ForumDTO> findAllByParentId(#Param("forumId")Integer id);
You just need to make sure that the foreign key to Parent is present on the entity.
Also notice the #Param annotation. It makes your parameters easier to track, and also does some basic type checking against the db. It's very useful to prevent SQL injection attacks, and UncheckedTypeExceptions.

Related

search collection by index in JPA #query

I have 2 entities with OneToMany Relation as below,
public class Visit{
#OneToMany(mappedBy = "visit", fetch = FetchType.LAZY)
#OrderBy("updated_on DESC")
private List<StatusChange> statusHistory;
}
public class StatusChange{
#ManyToOne
#JoinColumn(name = "ms_visit_id")
private Visit visit;
#Column(name = "to_status")
#Enumerated(EnumType.STRING)
private VisitStatus toStatus;
}
Visits can have multiple status like created, canceled, deleted etc.. whenever the status change there will be a new row added to the StatusChange table with a entry associated to that visit (toStatus will become canceled) . Now I want a query to filter the visits for which the latest status is canceled and with the matching user id.
I am using the #query of JPA. I already have got the result with the following query.
#Query(value = "select vs1.visit from StatusChange vs1 where vs1.id in (" +
" select max(vs2.id) from StatusChange vs2 " +
" where vs2.visit.user.id = :userId" +
" group by vs2.visit.id)" +
" and vs1.toStatus in :status")
public List<MSVisit> findByUserAndStatus(#Param("userId") Long userId, #Param("status") List<Visit.VisitStatus> status);
But I feel the query can be improved or is there any way to query some thing like,
"from Visit visit" +
" where visit.statusHistory.get(0).toStatus in :status" +
" and visit.user.id = :userId
Thanks for your help.
Usually, these queries are best modeled with lateral joins, but Hibernate does not support that yet. Note that unless you meant something different, your query is not correct, as the max-aggregation is based on the value of the id rather than the updated_on value. Not sure if that is on purpose, but I would suggest the following query in case you really want the latest visit.
#Query(value = "from Visit vs1 where vs1.user.id = :userId and (select max(h.updatedOn) from vs1.statusHistory h) >= ALL (" +
" select max(h.updatedOn) from vs1.statusHistory h " +
" and vs1.toStatus in :status")
public List<MSVisit> findByUserAndStatus(#Param("userId") Long userId, #Param("status") List<Visit.VisitStatus> status);

I can't convert query to native query using spring boot 2.3.3.RELEASE, but it's working when using 1.5.7.RELEASE

here's my query
SELECT
tb.id AS id,
tb.title AS title
FROM
tbl_batter tb
WHERE tb.`title` LIKE '%waugh%' ;
here's the code at repository
#Repository
public interface BatterRepository extends JpaRepository<Batter, Long> {
#Query(value = "SELECT \n" +
" tb.id AS id,\n" +
" tb.title AS title \n" +
"FROM\n" +
" tbl_batter tb \n" +
"WHERE tb.`title` LIKE %:searchKey%", nativeQuery = true)
List<Object[]> getData(#Param("searchKey") String searchKey);
}
Would be a huge favor if anyone could tell what I'm doing wrong while using version 2.3.3
I'm getting list empty while using 2.3.3
and getting list of 2 objects while using 1.5.7
I'm suppose to get 2 objects in the list and that's not happening in case of using 2.3.3
Try this:
#Repository
public interface BatterRepository extends JpaRepository<Batter, Long> {
#Query(value = "SELECT " +
" tb.id AS id," +
" tb.title AS title " +
"FROM" +
" tbl_batter tb " +
"WHERE tb.title LIKE '?1' ", nativeQuery = true)
List<Object[]> getData(String searchKey);
}

The #SqlResultSetMapping usage cause the schema validation problem

I have to extract data for statistic purpose. I've created a native query and used #SqlResultSetMapping to map the resultset to an object.
Hibernate needs to declare this class (Elaboration) as #Entity BUT IS NOT A TABLE, and I don't want a table because I have only to extract data on the fly when needed.
The code works fine but the gitlab pipeline fails during validation with
schemaManagementException: Schema-validation: missing table [elaboration].
Here my code so far:
SqlResultSetMapping(name="ValueMapping",
classes={
#ConstructorResult(
targetClass=Elaboration.class,
columns={
#ColumnResult(name="areadesc", type=String.class),
#ColumnResult(name="subsectordesc", type=String.class),
#ColumnResult(name="eurovalue", type=BigDecimal.class),
#ColumnResult(name="eurotch", type=BigDecimal.class),
}
)
})
#Entity
public class Elaboration{
#Id
private Long id;
private String areadesc;
private String subsectordesc;
private Integer dossiercount;
private BigDecimal eurovalue;
private BigDecimal eurotch;
....
and the custom query:
String statisticValueQuery = "select a.mdescr as areadesc, s.mdescr as subsectordesc, sum(euro_value) as eurovalue,
sum(euro_value_tch) as eurotch " +
"from dossier d " +
"join dossier_document dd on d.id = dd.dossier_id " +
"join dossier_country dc on d.id = dc.dossier_id " +
"join country c on dc.country_id = c.id " +
"join area a on c.area_id = a.id " +
"join dossier_subsector ds on d.id = ds.dossier_id " +
"join subsector s on ds.subsector_id = s.id " +
"where dd.document_id = :document " +
"and d.submission_date >= :startdate and d.submission_date <= :enddate " +
"group by s.id, a.id;";
public List<Elaboration> getValueElaboration(ElaborationRequestDTO elaborationRequestDTO){
Query resultMapping = em.createNativeQuery(statisticValueQuery, "ValueMapping");
resultMapping.setParameter("startdate", elaborationRequestDTO.getElaborateFromEquals());
resultMapping.setParameter("enddate", elaborationRequestDTO.getElaborateToEquals());
resultMapping.setParameter("document", elaborationRequestDTO.getDocumentIdEquals());
return resultMapping.getResultList();
Is there a way to pass the validation test?
Thanks
This is wrong statement.
Hibernate needs to declare this class (Elaboration) as #Entity
You should just put your #SqlResultSetMapping declaration above some #Entity but it can be some other entity not related to the Elaboration.
#SqlResultSetMapping(name="ValueMapping",
classes={
#ConstructorResult(
targetClass=Elaboration.class,
columns={
#ColumnResult(name="areadesc", type=String.class),
#ColumnResult(name="subsectordesc", type=String.class),
#ColumnResult(name="eurovalue", type=BigDecimal.class),
#ColumnResult(name="eurotch", type=BigDecimal.class),
}
)
})
#Entity
public class SomeEntity {
}
And if Elaboration is not an entity you should not annotate it as such.

JPA native query with same column twice

I am a JPA newbie and wanted to have a JPA native query for a single table (below) which I would like to fetch in my #Entity based class called TestRequest. It has a column 'RequestTime' that is fetched with DAYNAME() and then with DATEDIFF() functions.
SELECT TestRequest.Id AS Id
, TestRequest.RequestTime AS RequestTime
, DAYNAME(TestRequest.RequestTime) AS RequestDay
, TestRequest.StatusMessage AS StatusMessage
, DATEDIFF(CURDATE(), TestRequest.RequestTime) AS HowLongAgo
FROM TestRequest
LEFT JOIN TestRun
ON TestRequest.TestRunId = TestRun.Id
WHERE Requestor = '[NAME]'
ORDER BY Id DESC
Is there any way in which the column (fetched second time as HowLongAgo) be set into a property which is not mapped to a table column within the TestRequest class? Are there any field level annotations for this?
You need to use Interface-based projections:
You will need to create an interface that define the getters for each field in your projection like:
public interface RequestJoinRunProjection {
int getId();
LocalDate getRequestTime();
String getMessage();
String getRequestDay();
Long getHowLongAgo();
}
Then you define a method on your Repository that has the native query you want to run:
public interface TestRequestRepository extends CrudRepository<TestRequest, Long> {
// Any other custom method for TestRequest entity
#Query(value = "SELECT trq.Id AS id " +
" , trq.RequestTime AS requestTime " +
" , DAYNAME(trq.RequestTime) AS requestDay " +
" , trq.StatusMessage AS statusMessage " +
" , DATEDIFF(YEAR, CURDATE(), trq.RequestTime) AS howLongAgo " +
"FROM TestRequest trq " +
" LEFT JOIN TestRun tr " +
" ON trq.TestRunId = tr.Id " +
"WHERE Requestor = ?1 ORDER BY Id DESC"), nativeQuery = true)
List<RequestJoinRunProjection> findTestSumary(String name);
}
Notice query must be native since you are using database functions, also the column names must match the setters of your projection interface(following bean rules), so use AS in order to change the names in your query.
I strongly suggest you test your query on h2 before injecting into #Query annotation. DATEDIFF requires 3 parameters.

spring data jpa custom query fails to recognize class type

Not able to use custom POJO classes for my spring data jpa queries. Repeatedly fails with the following exception
"org.hibernate.MappingException: Unknown entity:
com.app.mycompany.AgileCenterServices.entities.ComponentDetailedInfo"*
Tried replacing the custom ComponentDetailedInfo.class and not mentioning anything during the call to entityManager.createNativeQuery(componentQuery.toString()), but then Object List returned fails to be converted to the specific POJO class after the query.
#Override
public ComponentListResponsePaginated findComponentByProjectId(String projectId, Pageable pageable) {
logger.info(" Inside findComponentByProjectId() API in IssueComponentServiceImpl");
String componentQuery = "select c.*, u.fullname "
+ "from issue_component c "
+ "left join user u on c.component_lead = u.username "
+ "where "
+ "upper(c.project_id) = upper(" + projectId + ")";
List<ComponentDetailedInfo> compList = new ArrayList<ComponentDetailedInfo>();
try {
logger.info(" ************* Printing query ******************************* ");
logger.info(componentQuery.toString());
compList = entityManager.createNativeQuery(componentQuery.toString(), ComponentDetailedInfo.class) .setFirstResult(pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
}
}
Also tried the following
List<? extends Object> objList = null;
objList = entityManager.createNativeQuery(componentQuery.toString()) .setFirstResult(pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
if(objList != null && objList.size() > 0) {
for(Object rec: objList) {
logger.info(" Printing Object ::: " + rec.toString());
compList.add((ComponentDetailedInfo)rec);
}
}
However the compList fails with the
java.lang.ClassCastException
The custom query returned should get typecast to the specific class type passed to the entityManager.createNativeQuery. However, I am facing the exception as mentioned above when I pass the class to createNativeQuery().
Even tried by totally removed the class in the createNativeQuery...
You have to define a constructor result mapping if you want to use a POJO as a result of a native query.
Here is an example query:
Query q = em.createNativeQuery(
"SELECT c.id, c.name, COUNT(o) as orderCount, AVG(o.price) AS avgOrder " +
"FROM Customer c " +
"JOIN Orders o ON o.cid = c.id " +
"GROUP BY c.id, c.name",
"CustomerDetailsResult");
And that's the mapping you have to add to your Entity:
#SqlResultSetMapping(name="CustomerDetailsResult",
classes={
#ConstructorResult(targetClass=com.acme.CustomerDetails.class,
columns={
#ColumnResult(name="id"),
#ColumnResult(name="name"),
#ColumnResult(name="orderCount"),
#ColumnResult(name="avgOrder", type=Double.class)})
})
If you don't like that approach you could use QLRM. Learn more about it here: https://github.com/simasch/qlrm

Resources