Spring Data JPA: CriteriaQuery to get entities with max value for each unique foreign key - sql

There's an Event class:
#Entity
public class Event {
#Id
private Integer id;
#ManyToOne(cascade = CascadeType.ALL)
private Company company;
#Column
private Long time;
...
}
I want to have an EventFilter class (implementing Specification) which will produce CriteriaQuery to select entities the same way as the following SQL query:
SELECT *
FROM events e1
WHERE e1.time = (
SELECT MAX(time)
FROM events e2
WHERE e1.company_id = c2.company_id
)
Filtered result will contain only events with unique Company and max time value per company.
This is the EventFilter class with what I ended up with:
public class EventFilter implements Specification<Event> {
#Override
public Predicate toPredicate(Root<Event> root, CriteriaQuery<?> q, CriteriaBuilder cb) {
Subquery<Long> subquery = q.subquery(Long.class);
Root<Event> subRoot = subquery.from(Event.class);
subquery.select(cb.max(root.get("time")))
.where(cb.equal(root.get("company"), subRoot.get("company")));
return cb.equal(root.get("time"), subquery);
}
}
When EventRepository#findAll(EventFilter filter) is called, results are not filtered at all. Please help me to implement this logic correctly.

After inspecting SQL statement generated by Hibernate I've found an error: root was used instead of subRoot. The correct method body is:
Subquery<Long> sub = q.subquery(Long.class);
Root<Event> subRoot = sub.from(Event.class);
sub.select(cb.max(subRoot.get("time")))
.where(cb.equal(root.get("company"), subRoot.get("company")));
return cb.equal(root.get("time"), sub);

Related

Spring Data Jpa, select based on two column values

Good Morning,
I have the following table:
_____________________
Name Status Num
A Good 6
B Bad 6
C Bad 7
I want to select all rows where "Status = 'Good' AND Num = '6'" OR "Status = 'Bad' AND Num = '7'"
So I would select rows with Names A and C from the above reference data.
I am hoping to be able to pass in two equal sized lists (ordered in the way I desire the query to be constructed), but have been unable to figure this out. The standard queries (SelectXByStatusAndNum) query generates SQL using 'IN' statements, and returns all 3 rows in the above data instead of just two.
Any insight appreciated
I believe the best way is to use a #Query annotation in the repository:
#Entity
#Table(name = "table_name")
public class TableName {
#Id
#Basic(optional = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "name")
private String name;
#Column(name = "status")
private String status;
#Column(name = "num")
private Integer num;
/**
* getters/setters, etc
**/
public interface TableNameRepository extends CrudRepository<TableName, Long> {
#Query("select t from TableName t where (status = :status1 and num = :num1 or status = :status2 and num = :num2)")
List<TableName> findByStatusAndNumOrStatusAndNum(#Param("status1") String status1,
#Param("num1") Integer num1, #Param("status2") String status2, #Param("num2") Integer num2);
}
It's a way to get values for a given parameters. In case of collections as parameters there is no in-box case due to RDBMS concept. You just can write some java code based on the key-valued parameter and collect results:
public interface TableNameRepository extends CrudRepository<TableName, Long> {
List<TableName> findByStatusAndNumIn(String status, Collection<Integer> nums);
}
List<TableName> result = new ArrayList<>();
List<TableName> itemGood = findByStatusAndNumIn("Good", numsGood);
List<TableName> itemBad = findByStatusAndNumIn("Bad", numsBad);
result.addAll(itemGood);
result.addAll(itemBad);
Instead of equal size lists, I suggest creating an object with both status and num and provide a list of those - I called it CustomCriteria below. Since there is no straightforward way to handle this with standard spring data query methods I believe the best way is to do this with criteria builder in a custom repository that builds the list of OR's from a list of AND predicates based on the criteria entries in the provided list. For example:
public List<YourTable> findMatching(List<CustomCriteria> customCriteriaList) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<YourTable> criteriaQuery = criteriaBuilder.createQuery(YourTable.class);
Root<YourTable> itemRoot = criteriaQuery.from(YourTable.class);
List<Predicate> predicates = new ArrayList<>(customCriteriaList.size());
for (CustomCriteria customCriteria : customCriteriaList) {
Predicate predicateForNum
= criteriaBuilder.equal(itemRoot.get("num"), customCriteria.getNum());
Predicate predicateForStatus
= criteriaBuilder.equal(itemRoot.get("status"), customCriteria.getStatus());
predicates.add(criteriaBuilder.and(predicateForNum, predicateForStatus));
}
Predicate finalPredicate = criteriaBuilder.or(predicates.toArray(new Predicate[predicates.size()]));
criteriaQuery.where(finalPredicate);
return entityManager.createQuery(criteriaQuery).getResultList();
}

SQLNative query returning empty results

I'm trying to execute a query which needs 4 tables :
#Query(value="SELECT e.* FROM erreur e, synop sy, synop_decode sd, station st WHERE e.id_synop = sd.id_synop_decode "
+ "and sd.id_synop_decode = sy.id_synop" + " and DATE(sy.date)= :date and "
+ "sy.id_station = st.id_station and st.id_station= :stationId", nativeQuery=true)
public List<Erreur> recherche(#Param("date") Date date, #Param("stationId") Long stationId);
This query works fine et native sql, i pass an existing stationId and a date like the following :
SELECT e.* FROM erreur e, synop sy, synop_decode sd, station st WHERE e.id_synop = sd.id_synop_decode and sd.id_synop_decode = sy.id_synop
and DATE(sy.date)= '2019-05-27' and sy.id_station = st.id_station and st.id_station= 60355;
This query works fine in Mysql Workbench.
Here's the actual controller i'm using for testing purpose :
#GetMapping("/station/{stationId}/erreurs/today")
public List<Erreur> getTodayErreurByStationId(#PathVariable Long stationId)
{
List<Erreur> erreurs = new ArrayList<Erreur>();
Optional<Station> stationOptional = stationRepository.findById(stationId);
if(stationOptional.isPresent())
{
return erreurRepository.recherche(new Date(), stationId);
}
return null;
}
The expected results are the actual "Ererur" objects in my array list, but RestClient just returns an empty array [], while the query works just fine in mysql like i described it above.
So my question is : How can i write this query into Hql language so that i can return the right entities. Or how can i map my sql results to my target custom calss "Erreur"?
#Entity
#Getter #Setter #NoArgsConstructor
#Table(name="erreur")
public class Erreur {
public Erreur(int section, int groupe, String info) {
this.section = section;
this.groupe = groupe;
this.info = info;
}
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="id_erreur")
private Long id;
#ManyToOne(cascade= {CascadeType.DETACH,CascadeType.MERGE,CascadeType.PERSIST,CascadeType.REFRESH},
fetch=FetchType.LAZY)
#JsonIgnore
#JoinColumn(name="id_synop")
private SynopDecode synopDecode;
#OneToOne
#JoinColumn(name="id_controle")
private Controle controle;
#ManyToOne(cascade= {CascadeType.DETACH,CascadeType.MERGE,CascadeType.PERSIST,CascadeType.REFRESH},
fetch=FetchType.LAZY)
#JsonIgnore
#JoinColumn(name="id_station")
private Station station;
#Column(name="section")
private int section;
#Column(name="groupe")
private int groupe;
#Column(name="info")
private String info;
}
If you want to use jpa convention directly then you will have to make associations between different entities i.e. how two entities are linked. When we define these associations then spring jpa knows how to convert method name or custom queries into SQL.
Your code will need to be something like
public class Erreur {
...
#ManyToOne
#JoinColumns//define how Erreur and SynopeDecone are linked
private SynopDecode synopDecode;
...
public class SynopDecode {
...
#ManyToOne // or #OneToOne its not mentioned in question how these two are linked
#JoinColumns//define how SynopDecode and Synop are linked
private Synop synop;
...
Then you can write your query like
#Query("select e from Erreur e LEFT JOIN e.synopDecode sy LEFT JOIN sy.synop sy WHERE DATE(sy.date) = :date AND sy.id_station = :stationId")
List<Erreur> getByDateAndStationId(#Param("date") Date date, #Param("stationId") Long stationId)
You can't use method name based query because you want to use SQL function to match only "date" part of your date and not the whole timestamp.
You can use jpa methods by conventions.
Assuming SynopDecode has property like:
//SynopDecode class
#ManyToOne
private Synop synop;
//repository interface
List<Erreur> findByStationIdAndSynopDecodeSynopDate(Long stationId, Date date);
//or
//List<Erreur> findByStationIdAndSynopDecode_Synop_Date(Long stationId, Date date);
UPDATE
As Punit Tiwan (#punit-tiwan) note that, the above methods used for a specific datettime.
You can use methods below for just DATE.
//repository interface
List<Erreur> findByStationIdAndSynopDecodeSynopDateBetween(Long stationId, Date startOfDate, Date endOfDate);
//or
//List<Erreur> findByStationIdAndSynopDecode_Synop_DateBetween(Long stationId, Date startOfDate, Date endOfDate);
I figured a way to get the same results as my SQL Query using the #Query annotation and accessing object properties like this :
#Query("from Erreur e where e.synopDecode.synop.station.id = :stationId and "
+ "DATE(e.synopDecode.synop.date) = :date")
public List<Erreur> recherche(#Param("date") Date date, #Param("stationId") Long stationId);
I think it solves my problem, thanks for the help

Ignite SqlQuery for complex java objects

In my cache I have a complex java object as below -
class Person{
private Department d;
....
}
class Department {
private Department code;
....
}
I am using below SQLQuery to read it -
SqlQuery<Short, BinaryObject> query = new SqlQuery<>(Person.class, "d.code = ?");
String args="101"; // department code
QueryCursor<Cache.Entry<Short, BinaryObject>> resultSet = personCache.query(query.setArgs(args))
I am getting below error -
Caused by: class org.apache.ignite.internal.processors.query.IgniteSQLException: Failed to parse query: SELECT "PERSON_CACHE"."PERSONENTITY"._KEY, "TPERSON_CACHE"."PERSONENTITY"._VAL FROM "PERSON_CACHE"."PERSONENTITY" WHERE id.code = ?
Am I doing anything wrong here ?
You can access nested fields, but only if they were configured with QuerySqlField annotation in advance:
class Person{
private Department d;
...
}
class Department {
#QuerySqlField
private Department code;
....
}
SqlQuery<Short, BinaryObject> query = new SqlQuery<>(Person.class, "code = ?");
Destructuring is not supported by Ignite SQL and there are no solid plans to implement it.
This means you can't peek into fields that are rich objects, maps, lists, etc. You should introduce a departmentId numeric field here.
Theoretically you could also try putting #QuerySqlField annotation on Department's field code, and then access it as CODE = ?. Your mileage may vary. I for one would like to hear about the result of such experiment.
I resolved it by using predicate.
IgniteBiPredicate<Long, BinaryObject> predicate = new IgniteBiPredicate<Long, BinaryObject>() {
#Override
public boolean apply(Long e1, BinaryObject e2) {
Person p= e2.deserialize();
short s = (short) args[0];
return p.getId().getCode == s;
}
};

Retrieving entities by checking a filtered subset of childen

I have entities of the following structure:
#Entity
public Class MyObject {
...
public List<Status> statuses;
...
}
#Entity
public Class Status {
...
public Long creationTime;
public Double range;
#JoinColumn(name = "myObject",
nullable = false,
unique = false,
updatable = false)
#ManyToOne(optional = false)
#NotNull
private MyObject myObject;
...
}
I got n entities of the type MyObject which can have m statuses. Ownership lays on the side of the Status entity which cannot be changed. I am currently trying to filter the entities that are greater-equal than a certain range. My current query retrieves a result for every status that has a range that is greater-equal than the one given as a parameter:
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<MyObject> cq = cb.createQuery(MyObject.class);
final Root<MyObject> myObject = cq.from(MyObject.class);
final Root<Status> statuses = cq.from(Status.class);
final Predicate[] predicates = {
cb.equal(myObject.get(MyObject_.id), statuses.get(Status_.myObject).get(MyObject_.id)),
cb.ge(statuses.get(Status_.range), range)
};
cq.select(myObject).where(predicates);
return entityManager.createQuery(cq).getResultList();
Desired is, that I only get the entities of MyObject, for which the most current status that has a range is greater-equal than the one given as a paramter. I am pretty sure that I have to group the list of statuses first and sort by creation time but I am not really sure how to combine all of this, especially not in the Criteria API.
EDIT:
At least I was able to create a SQL query that does what I want:
SELECT * FROM MyObject O INNER JOIN (SELECT * FROM Status WHERE Status.CREATIONTIME IN (SELECT MAX(Status.CREATIONTIME) FROM Status WHERE (Status.RANGE IS NOT NULL AND Status.RANGE >= 30) GROUP BY Status.myObject)) S ON O.ID = S.myObject;
I am trying to rebuild this query using the Criteria API but don't really know how to include the embedded SELECT in the JOIN. Any help would be appreciated!

Join query in hibernate search

I want to write a lucene query like
" from activity where metaentityByEntity.id in(select metaentityByEntity.id from userentity where user.id=1)"
My domain classes are:
public class Activity implements java.io.Serializable {
private Long id;
private Date timeStamp;
private User user;
#IndexedEmbedded
private Metaentity metaentityByEntity;
}
public class Userentitydetail implements java.io.Serializable {
private Long id;
private Date timeStamp;
private Metaentity metaentityByEntity;
#IndexedEmbedded
private User user;
private Metaentity metaentityByProjectId;
private byte unfollow;
private Byte isAssociated;
}
But how to write lucene query which will search from multiple indexes? basically I am doing hibernate search.
Thanks.
Lucene is not a relational database, so the short answer is you shouldn't try doing joins; your specific use case happens to be implementable because your query can be greatly simplified.
Just create a query on the field which is following the link:
QueryBuilder queryBuilder = fullTextSession.getSearchFactory()
.buildQueryBuilder()
.forEntity( Activity.class )
.get();
Query query = queryBuilder.keyword()
.onField( "metaentityByEntity.user.id" )
.ignoreAnalyzer()
.matching( 1 )
.createQuery();
You'll have to adjust some details as you omitted some mapping details; for example it might need to be
.matching( "1" )
instead.