JPA CriteriaBuilder: ListJoin with IN query on joined cloumn - java

I have a use-case where I need to search a table on a list of values. Below is the schema:
CASE table
Case_Id
CASE_CIN table
Case_Id
cin
CASE & CASE_CIN table are joined via Many to Many.
I need to search cases based on provided list of CINs. This is the SQL that I'm trying to implement:
select distinct c.* from case c left join case_cin cc on c.case_id = cc.case_id where cc.cin in ("cin1", "cin2", "cin3");
This is how I designed my filter criteria based on just a single CIN:
public static Specification<CaseEntity> buildSpecification(CaseSearchRequestDto criteria, String userName) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (!isEmpty(criteria.getAssociatePartyCins())) {
predicates.add(buildAssociatePartyCinsFilterPredicate(root, cb, query, criteria.getAssociatePartyCins());
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
private static Predicate buildAssociatePartyCinsFilterPredicate(Root<CaseEntity> root, CriteriaBuilder cb, CriteriaQuery query, List<String> cins) {
Predicate filterPredicate = cb.disjunction();
ListJoin<CaseEntity, String> cinsJoin = root.joinList(FIELD_CASE_CINS, LEFT);
filterPredicate.getExpressions().add(startWithString(cinsJoin, cb, **cins.get(0)**); // need to change logic here.
query.distinct(true);
return filterPredicate;
}
I'd also like to have the exact match for every CIN rather that startWithString.
Can anyone help modify the code to allow search by multiple values?

It turns out to be way simpler than I thought. Just need to replace the condition with cinsJoin.in(cins).
private static Predicate buildAssociatePartyCinsFilterPredicate(Root<CaseEntity> root, CriteriaBuilder cb, CriteriaQuery query, List<String> cins) {
Predicate filterPredicate = cb.disjunction();
ListJoin<CaseEntity, String> cinsJoin = root.joinList(FIELD_CASE_CINS, LEFT);
filterPredicate.getExpressions().add(cinsJoin.in(cins));
query.distinct(true);
return filterPredicate;
}

Related

get fields in join path with criteria

I wanna know is there a way to do something like this in hibernate using criteriaBuilder
select users.first_name,orders.payable,order_item.product_title
from "order" orders
join users on orders.user_id_fk = users.id_pk
join order_item on orders.id_pk = order_id_fk
I need this specially if I have to use group by. I search and google and also read this article but have no clue how can I do this in hibernate:
Query Selection Other Than Entities
querycriteria
hibernate-facts-multi-level-fetching
also I code this for selecting field in first layer and it worked perfectly for selecting first layer but it need some change to work with join and select field from other table than root:
<R> List<R> reportEntityGroupBy(List<String> groupBy, List<String> selects, Class<T> root, Class<R> output) {
CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
CriteriaQuery<R> criteriaQuery = criteriaBuilder.createQuery(output);
Root<T> rootQuery = criteriaQuery.from(root);
if (selects == null || selects.isEmpty()) {
selects = groupBy;
}
criteriaQuery.multiselect(selects.stream().map(rootQuery::get).collect(Collectors.toList()));
criteriaQuery.groupBy(groupBy.stream().map(rootQuery::get).collect(Collectors.toList()));
I use Hibernate 5.4.22.Final and use entityGraph for my join.
I don't know how your selects look like, but I suppose you are using paths i.e. attributes separated by .. In that case, you have to split the paths and call Path#get for each attribute name.
List<Path> paths = selects.stream()
.map(select -> {
Path<?> path = rootQuery;
for (String part : select.split("\\.")) {
path = path.get(part);
}
return path;
})
.collect(Collectors.toList()));
criteriaQuery.multiselect(paths);
criteriaQuery.groupBy(paths);

How to Implement sum in CriteriaQuery

I want to fetch the data with the sum of one column and group by two fields from the database where the parameters will be dynamic
I tried to implement using predicates I am able to get the group by but sum is not working.
Page<PaymentDetail> aggregatedPaymentDetails2 = paymentDetailRepository.findAll(new Specification<PaymentDetail>() {
#Override
public Predicate toPredicate(Root<PaymentDetail> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
final List<Predicate> predicates = new ArrayList<>();
query.multiselect(root.get("id"), root.get("lockVersion"), cb.sum(root.get("amountPaid")), root.get("referenceNumber"), root.get("paymentSlot"));
query.groupBy(root.get("referenceNumber"), root.get("paymentSlot").get("id"));
for (final QueryCriterion queryCriterion : queryCriterionList) {
final OperatorEnum operatorEnum = queryCriterion.getOperatorEnum();
From join = root;
final String[] attributes = queryCriterion.getKey().split("\\.");
for (int i = 0, attributesLength = attributes.length - 1; i < attributesLength; i++) {
join = join.join(attributes[i], JoinType.LEFT);
}
final Path path = join.get(attributes[attributes.length - 1]);
final Object value = dataTypesHelper.typeCastValue(path, queryCriterion);
predicates.add(operatorEnum.getOperator().getPredicateByKeyAndValue(path, value, cb));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
},pageable);
I expect the output to be page with sum of amountpaid and group by paymentslotid, PaymentSlot has a onetomany relationship with paymentdetails
Specifications are intended to produce a Predicate which is essentially a where clause.
That you also can trigger side effects through the JPA API is an unfortunate weakness of the JPA API. There is no support in Spring Data JPA to change the return value through a Specification. Use a custom method implementation for this.

JPA Hibernate Order by alphanumeric field

I am using Jpa/Hibernate to access MariaDB in a Spring Boot application
I am strugling to sort data by an alphanumeric field containng numbers that might end with one letter (pattern \d+[a-z]?)
e.g.
10
104
20a
100b
and I need them ordered like this
10
20a
100b
104
I a bulding my own query with the Criteria Api because I also have to do some complex filtering.
#Transactional(readOnly = true)
public class EntryRepositoryImpl implements EntryRepositoryCustom {
#PersistenceContext
private EntityManager entityManager;
#Override
public Page<Entry> get(MultiValueMap<String, String> parameters, Pageable pageable) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// count total number of filterd entries
Long totalResultCount = getResultCount(parameters, criteriaBuilder);
// build query to get filterd entries
CriteriaQuery<Entry> selectQuery = criteriaBuilder.createQuery(Entry.class);
Root<Entry> getRoot = selectQuery.from(Entry.class);
CriteriaQuery<Entry> select = selectQuery.select(getRoot);
addFilters(parameters, criteriaBuilder, getRoot, select);
// add sorting
List<javax.persistence.criteria.Order> sortOrders = JpaUtils.translateSorting(pageable,
getRoot);
select.orderBy(sortOrders);
// get one page of filterd entries
List<Entry> results = getPageResults(pageable, select);
return new PageImpl<>(results, pageable, totalResultCount);
}
/**
* Translate spring to jpa sorting.
*/
public static List<javax.persistence.criteria.Order> translateSorting(Pageable pageable,
Root<Entry> root) {
List<Sort.Order> orders = new ArrayList<>();
if (pageable.getSort() != null) {
pageable.getSort().iterator().forEachRemaining(orders::add);
}
return orders.stream().
map(order -> {
String[] parts = order.getProperty().split("\\.");
String field = parts[0];
Path path = parts.length == 2 ? root.join(field).get(parts[1]) : root.get(field);
return new OrderImpl(path, order.isAscending());
})
.collect(Collectors.toList());
}
I already have a custom comparator but it seems, there is no way to translate it so the DB could use it.
So far I found the following solutions/ideas
using #SortComparator, but it is not feasible for my use case because the ordering has to happen in the database, because there are over 500k complex rows.
this sql base solution but don't know how to translate it into the Criteria Api.
after looking at the function of CriteriaBuilder (javadoc) I got the idea to split the value into the numeric and string parts and apply to orders but there is not function to split with a regular expression.
Edit:
For now I did split the field into 2 and use two sort expression.

Generate a “where in” statement using the Criteria API in Hibernate

I put in place the following specification that represents the predicate construction for querying Students based on their age and their ClassRoom's teachers' name (one student can have one or more classroom)
public class StudentSpecification implements Specification<Student> {
private final Integer age;
public StudentSpecification(Integer age){
this.age = age;
}
#Override
public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.<Integert>get(age), Integer.valueOf(v)));
SetJoin<Student, ClassRoom> classRooms = root.join(Student_.classRooms);
predicates.add(criteriaBuilder.equal(classRooms.get(ClassRoom_.teacher), "Marta"));
predicates.add(criteriaBuilder.equal(classRooms.get(ClassRoom_.teacher), "Fowler"));
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
}
}
Here is an example of data :
Student
_____________________________________________
ID CLASSROOM_ID NAME AGE
2 120 Pascal 22
8 120 Bryan 21
ClassRoom
_____________________________________________
ID CLASSROOM_ID TEACHER
1 120 Marta
2 120 McAllister
2 120 Fowler
The specification returns nothing.
When I see the generated statement, I understand why it doesn't work :
where
classRooms.teacher=?
and classRooms.teacher=?
I was expecting something like :
where
students0.classroom_id in (
select classrooms0.classroom_id where
classRooms.teacher=?
)
and students0.classroom_id in (
select classrooms0.classroom_id where
classRooms.teacher=?
)
Question : how can make a query with the Criteria API work in my case ?
You will need Subquery to achieve what you want if you need to stick with Criteria API. Otherwise, HQL can be a better choice for the sake of readability compared to the verbosity of Criteria API.
The idea is to generate individual queries and make a manual join through a predicate. So no need for a Join or SetJoin.
First, note that there are some mistakes in your code. The most obvious one is the path you used to reach the age field. You should use the generated metamodel instead of hard coded strings.
predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get(Student_.age), age));
instead of :
predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.<Integert>get(age), Integer.valueOf(v)));
Then, here is the complete solution :
public static Specification<Student> withTeacherAndName(){
return new Specification<Student>() {
#Override
public Predicate toPredicate(Root<Student> root, CriteriaQuery<?> criteriaQuery,
CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get(Student_.age), 20));
Subquery<String> sq1 = criteriaQuery.subquery(String.class);
Root<Classroom> classroomRoot = sq1.from(Classroom.class);
sq1.select(classroomRoot.get(Classroom_.classroomId));
sq1.where(criteriaBuilder.equal(classroomRoot.get(Classroom_.teacher), "Marta"));
Subquery<String> sq2 = criteriaQuery.subquery(String.class);
Root<Classroom> classroomRoot2 = sq2.from(Classroom.class);
sq2.select(classroomRoot2.get(Classroom_.classroomId));
sq2.where(criteriaBuilder.equal(classroomRoot2.get(Classroom_.teacher), "Fowler"));
criteriaQuery.where(criteriaBuilder.equal(root.get(Student_.classroomId), sq1));
criteriaQuery.where(criteriaBuilder.equal(root.get(Student_.classroomId), sq2));
return criteriaBuilder.and(predicates.toArray(new Predicate[]{}));
}
};
}
So basically you are creating a subquery for each criteria.
The code needs a refactoring (a loop for example).
If you want an in Clause instead of an equals clause just use it:
predicates.add(criteriaBuilder.in(classRooms.get(ClassRoom_.teacher), "Marta"));
predicates.add(criteriaBuilder.in(classRooms.get(ClassRoom_.teacher), "Fowler"));
See https://docs.oracle.com/javaee/6/api/javax/persistence/criteria/CriteriaBuilder.html#in(javax.persistence.criteria.Expression)

How to sort a query result by their date (Jpa, Java Collections)

public List<Movie> findRange(int[] range) {
CriteriaQuery cq = em.getCriteriaBuilder().createQuery();
cq.select(cq.from(Movie.class));
Query q = em.createQuery(cq);
q.setMaxResults(range[1] - range[0]);
q.setFirstResult(range[0]);
List<Movie> list1 = q.getResultList();
Collections.sort(list1, new Comparator(){
public int compare (Object o1, Object o2){
Movie p1 = (Movie)o1;
Movie p2 = (Movie)o2;
return p2.getDate().compareTo(p1.getDate());
}
});
return list1;
}
As it is now sorting works, but only with in the batch,
(batch = 4 movies) First one = last entered; Last one = fisrt entered.
But only compared with those in the batch not all of them as needed.
Thank you very much for any help you can provide
best Regards
Ignacio
You should probably define the order in the criteria query and not sort the results after the query has already been executed. Otherwise you will end up sorting only a part of the results.
You can assign sort order to the query using CriteriaQuery.orderBy(Order ...). The order by expression can be created with the CriteriaBuilder using either asc(Expression) for ascending or desc(Expression) for descending order.
The following might or might not work:
public List<Movie> findRange(int[] range) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Movie> cq = cb.createQuery(Movie.class);
Root<Movie> movie = cq.from(Movie.class);
cq.select(movie);
cq.order(cb.asc(movie.get(Movie_.getDate()));
...
}

Categories