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

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)

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);

JPA CriteriaBuilder: ListJoin with IN query on joined cloumn

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;
}

Dynamic query by using criteria builder in Spring Data [duplicate]

Spring-data, Oliver Gierke's excellent library, has something called a Specification (org.springframework.data.jpa.domain.Specification). With it you can generate several predicates to narrow your criteria for searching.
Can someone provide an example of using a Subquery from within a Specification?
I have an object graph and the search criteria can get pretty hairy. I would like to use a Specification to help with the narrowing of the search, but I need to use a Subquery to see if some of the sub-elements (within a collection) in the object graph meet the needs of my search.
Thanks in advance.
String projectName = "project1";
List<Employee> result = employeeRepository.findAll(
new Specification<Employee>() {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Subquery<Employee> sq = query.subquery(Employee.class);
Root<Project> project = sq.from(Project.class);
Join<Project, Employee> sqEmp = project.join("employees");
sq.select(sqEmp).where(cb.equal(project.get("name"),
cb.parameter(String.class, projectName)));
return cb.in(root).value(sq);
}
}
);
is the equivalent of the following jpql query:
SELECT e FROM Employee e WHERE e IN (
SELECT emp FROM Project p JOIN p.employees emp WHERE p.name = :projectName
)

JPA Specifications: Select items where list attribute contains item with certain value

I am trying to solve the following problem with JPA specifications:
Consider the following classes:
public class Language {
private String name;
...
}
public class Person {
private List<Language> languages;
...
}
How do I select all Persons, that speak a language with name x? I am looking for a solution using the CriteriaBuilder, which should generate a Predicate that I can 'and' together with other predicates.
Thanks in advance.
It is actually pretty easy:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> query = cb.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
ListJoin<Person, Language> langJoin = root.join(Person_.langs);
query.where(cb.equal(langJoin.get(Language_.name), lang));
So you basically join the languages via corresponding association and then add an equals predicate matching the required attribute of the joined entity with your criteria.

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.

Categories