JPA Criteria API with multiple parameters - java

I need to make a search method that uses the JPA Criteria API with multiple parameters.
Now the problem is that not every parameter is required. So some could be null, and they shouldn't be included in the query. I've tried this with the CriteriaBuilder but I couldn't see how to make it work.
With the Hibernate Criteria API this is fairly easy. Just create the criteria and then add Restrictions.
Criteria criteria = session.createCriteria(someClass.class);
if(someClass.getName() != null) {
criteria.add(Restrictions.like("name", someClass.getName());
}
How could I achieve the same with JPA's Criteria API?

Concept is to construct array of javax.persistence.Predicate which contains only predicates we want to use:
Example entity to be queried:
#Entity
public class A {
#Id private Long id;
String someAttribute;
String someOtherAttribute;
...
}
Query itself:
//some parameters to your method
String param1 = "1";
String paramNull = null;
CriteriaBuilder qb = em.getCriteriaBuilder();
CriteriaQuery cq = qb.createQuery();
Root<A> customer = cq.from(A.class);
//Constructing list of parameters
List<Predicate> predicates = new ArrayList<Predicate>();
//Adding predicates in case of parameter not being null
if (param1 != null) {
predicates.add(
qb.equal(customer.get("someAttribute"), param1));
}
if (paramNull != null) {
predicates.add(
qb.equal(customer.get("someOtherAttribute"), paramNull));
}
//query itself
cq.select(customer)
.where(predicates.toArray(new Predicate[]{}));
//execute query and do something with result
em.createQuery(cq).getResultList();

A simple solution for Spring, using lambda expressions:
Specification<User> specification = (root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
// like
predicates.add(builder.like(root.get("name"), "%test%"));
// equal
predicates.add(builder.equal(root.get("parent_id"), 99L);
// AND all predicates
return builder.and(predicates.toArray(new Predicate[0]));
};
repository.findAll(specification);

Take a look at this site JPA Criteria API. There are plenty of examples.
Update: Providing a concrete example
Let's search for Accounts with a balance lower than a specific value:
SELECT a FROM Account a WHERE a.balance < :value
First create a Criteria Builder
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Account> accountQuery = builder.createQuery(Account.class);
Root<Account> accountRoot = accountQuery.from(Account.class);
ParameterExpression<Double> value = builder.parameter(Double.class);
accountQuery.select(accountRoot).where(builder.lt(accountRoot.get("balance"), value));
To get the result set the parameter(s) and run the query:
TypedQuery<Account> query = entityManager.createQuery(accountQuery);
query.setParameter(value, 1234.5);
List<Account> results = query.getResultList();
BTW: The entityManager is injected somewhere in an EJB/Service/DAO.

Mikko's answer worked beautifully. Only change I needed to do, was to replace:
cq.select(customer).where(predicates.toArray(new Predicate[]{}));
with:
Predicate [] predicatesarr = predicates.toArray(new Predicate[predicates.size()]);
cq.select(customer).where(predicatesarr);
Somewhere the conversion from list to array in the original did not work.

First, Mikko's answer got me to my answer. Upvote for that.
My scenario was I wanted to parent/child relationship and I wanted to find a match on ~any~ child.
Employee has multiple JobTitle(s).
I wanted to find an employee (where the has many job titles), but find it on ~any of the jobtitles I send in.
SQL would look like:
Select * from dbo.Employee e join dbo.JobTitle jt on e.EmployeeKey = jt.EmployeeKey
WHERE ( jt.JobTitleName = 'programmer' OR jt.JobTitleName = 'badcop' )
I threw in gender and date-of-birth to complete the example (and give more "optional") criteria)
My JPA code
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;
public class MyEmployeeSpecification implements Specification<MyEmployee> {
private MyEmployee filter;
public MyEmployeeSpecification(MyEmployee filter) {
super();
this.filter = filter;
}
public Predicate toPredicate(Root<MyEmployee> root, CriteriaQuery<?> cq,
CriteriaBuilder cb) {
Predicate returnPred = cb.disjunction();
List<Predicate> patientLevelPredicates = new ArrayList<Predicate>();
if (filter.getBirthDate() != null) {
patientLevelPredicates.add(
cb.equal(root.get("birthDate"), filter.getBirthDate()));
}
if (filter.getBirthDate() != null) {
patientLevelPredicates.add(
cb.equal(root.get("gender"), filter.getGender()));
}
if (null != filter.getJobTitles() && filter.getJobTitles().size() > 0) {
List<Predicate> jobTitleLevelPredicates = new ArrayList<Predicate>();
Join<JobTitle, JobTitle> hnJoin = root.join("jobtitles");
for (JobTitle hnw : filter.getJobTitles()) {
if (null != hnw) {
if (StringUtils.isNotBlank(hnw.getJobTitleName())) {
jobTitleLevelPredicates.add(cb.equal(hnJoin.get("getJobTitleName"), hnw.getFamily()));
}
}
}
patientLevelPredicates.add(cb.or(jobTitleLevelPredicates.toArray(new Predicate[]{})));
}
returnPred = cb.and(patientLevelPredicates.toArray(new Predicate[]{}));
return returnPred;
}
}
But I figured mine out because of predicates.toArray(new Predicate[]{}) , aka, the varargs trick. (Thanks Mikko)
I'm also doing the "implements Specifiction" method.
Other helpful links:
JPA Specifications by Example
JPA CriteriaBuilder conjunction criteria into a disjunction criteria

Related

Build WHERE clause dynamically based on filter in queryDSL

I have a query that needs a dynamic SQL WHERE clauses based on filters.
Issue
It may give me a NullPointerException (NPE) if I'm adding an "AND" while the previous filter is not existing.
Code
public List<BlocageDeblocageCompte> getMyQuery(FiltersDTO filters) {
JPAQuery<MY_ENTITY> myEntity = getJPAQueryFactory().selectFrom(myEntity).limit(20);
BooleanExpression whereClause = null;
boolean whereClauseAdded = false;
boolean ifNoFilter = Stream.of(myEntity).allMatch(Objects::isNull);
if (ifNoFilter) {
return new ArrayList<>(myEntity.fetchAll().fetch());
}
if (filters.getId() != null) {
whereClause = myEntity.id.eq(filters.getId());
whereClauseAdded = true;
}
if (filters.getName() != null) {
if (whereClauseAdded) {
whereClause = whereClause.and(myEntity.id.eq(filters.getName()));
} else {
whereClause = myEntity.id.eq(filters.getName());
whereClauseAdded = true;
}
}
// remaining code
}
Question
Is there a better way to add the filters, without risking a NPE?
To construct complex boolean queries like this you can use com.querydsl.core.BooleanBuilder:
public List<MyEntity> getMyQuery(FiltersDTO filters) {
MyEntity myEntity = QModel.myEntity;
// build an optional WHERE clause with predicates using AND conjunctions
BooleanBuilder builder = new BooleanBuilder();
if (filters.getId() != null) {
builder.and(myEntity.id.eq(filters.getId()));
}
if (filters.getName() != null) {
builder.and(myEntity.id.eq(filters.getName()));
}
// construct the query
return getJPAQueryFactory().selectFrom(myEntity)
.limit(20)
.where(builder) // myEntity.id eq id1 AND myEntity.name eq name1
.fetch();
}
See also
Similar questions:
Dynamic search term SQL query with Spring JPA or QueryDSL
JPA QueryBuilder
How to write JPA query with boolean condition
References:
QueryDSL Reference Guide: 3.1.1. Complex predicates
QueryDSL Issue on GitHub: BooleanBuilder with multiple AND conditions · Discussion #2936
Baeldung's Tutorial: A Guide to Querydsl with JPA
You can implement the Specification interface in JPA:
first of all, create a Filter class with static predicate-methods to handle fields in WHERE clause:
public class Filter{
public static Specification<BlocageDeblocageCompte> hasName(String name) {
return (root, query, cb) -> {
if (name == null || name.equals("")) {
return cb.isTrue(cb.literal(true)); // always true = no filtering
}
return cb.like(root.get("name"), "%" + name + "%");
};
}
public static Specification<ActiveAlarm> hasId(Integer id) {
return (root, query, cb) -> {
if (id == null) {
return cb.isTrue(cb.literal(true)); // always true = no filtering
}
return cb.equal(root.get("id"), id);
};
}
}
then, use the Filter class in your methods:
repository.findAll(where(Filter.hasName(filters.getName()).and(Filter.hasId(filters.getId()));
For more details, see:
Baeldung:
REST Query Language with Spring Data JPA Specifications
Spring blog: Advanced Spring Data JPA - Specifications and Querydsl
Kailas Nath on Medium: Using JPA Specification Interface To Implement Dynamic Filters
DZone: Java: Using the Specification Pattern With JPA
I found out a simple solution, when we do WHERE on all of them it transform automatically the second where to AND. thank u guys.
JPAQuery<MyEntity> myEntityJPAQUERY=
getJPAQueryFactory()
.selectFrom(QmyEntity);
if (filtresDTO.getId() != null) {
myEntityJPAQUERY=myEntityJPAQUERY.where(QmyEntity.id.eq(ffiltresDTO.getId()));
if (filtresDTO.getName() != null) {
myEntityJPAQUERY=myEntityJPAQUERY.where(QmyEntity.name.eq(ffiltresDTO.getName()))
}

How to simplify the code of two similar methods?

I would like to refactor these two methods which are practically the same except for a "maxResult ()", these 2 methods refer to two different gets, one that returns me the single user and one that returns the list instead. How could I simplify these two methods (always if it makes sense)
these are the 2 methods:
First Method:
public List findFirstByTransactionId(String transactionId) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
criteria.select(root).where(builder.equal(root.get(User_.transactionId), transactionId));
criteria.orderBy(builder.asc(root.get(User_.date)));
TypedQuery<User> query = em.createQuery(criteria).setMaxResults(1);
return query.getSingleResult();
Second Method:
public List<User> findAllByTransactionId(String transactionId) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
criteria.select(root).where(builder.equal(root.get(User_.transactionId), transactionId));
criteria.orderBy(builder.asc(root.get(User_.date)));
TypedQuery<User> query = em.createQuery(criteria);
return query.getResultList();
More of an addendum.
The other answers suggesting to add a boolean parameter are valid, but: clean coding advises to always strive for minimal count of parameters. And especially such boolean parameters are discouraged. Of course, it still makes sense here to do it that way, to avoid code duplication.
But what I would do:
yes, internally have a private List<User> findFirstByTransactionId() taking a boolean parameter
but on your public interface, simply offer TWO different methods, like public List<User> findFirstUserByTransactionId() and `public List findUsersByTransactionId()``
Those two public methods can then call the internal method and pass true/false. Using a boolean to make that decision is an implementation detail, and you should avoid making that visible on the public side of things.
Well the easiest option usually is to try to move the code that is the same to its own method. Here, you have multiple options, one of them could be something like this:
Write a new (private) method:
private List<User> findFirstByTransactionId(String transactionId, boolean onlyOneResult) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
criteria.select(root).where(builder.equal(root.get(User_.transactionId), transactionId));
criteria.orderBy(builder.asc(root.get(User_.date)));
TypedQuery<User> query = em.createQuery(criteria);
if (onlyOneResult) {
query = query.setMaxResults(1);
}
return query.getResultList();
}
And then refactor your existing methods like this:
public List<User> findFirstByTransactionId(String transactionId) {
return findFirstByTransactionId(transactionId, false);
}
public List<User> findFirstByTransactionId(String transactionId) {
return findFirstByTransactionId(transactionId, true);
}
Now you eliminated 8 lines of duplicate code :)
The methods you call on query work just like any other methods, so you can just put some of the calls inside an if block like this:
public List<User> findByTransactionId(String transactionId, boolean onlyFirst) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
criteria.select(root).where(builder.equal(root.get(User_.transactionId), transactionId));
criteria.orderBy(builder.asc(root.get(User_.date)));
TypedQuery<User> query = em.createQuery(criteria);
if (onlyFirst) {
query=query.setMaxResults(1);
}
return query.getResultList();
}

JpaSpecificationExecutor JOIN + ORDER BY in Specification

I have a query using a JOIN and ORDER BY and want to use it within my repository using the Criteria Api.
Here I found, how to wrap such a query into a CriteriaQuery (Link).
CriteriaQuery<Pet> cq = cb.createQuery(Pet.class);
Root<Pet> pet = cq.from(Pet.class);
Join<Pet, Owner> owner = cq.join(Pet_.owners);
cq.select(pet);
cq.orderBy(cb.asc(owner.get(Owner_.lastName),owner.get(Owner_.firstName)));
On the other side, I found some examples to use the Criteria Api in Combination with a JpaRepository (example).
The Problem is that all methods in the repository expect a Specification:
T findOne(Specification<T> spec);
which is always build like this:
public static Specification<PerfTest> statusSetEqual(final Status... statuses) {
return new Specification<PerfTest>() {
#Override
public Predicate toPredicate(Root<PerfTest> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.not(root.get("status").in((Object[]) statuses));
}
};
}
So at one side I know how to create a CriteriaQuery, and on the other side I need a Specification which is build from a Predicate, and I can not figure out how to parse the CriteriaQuery into a Specification/Predicate.
Try something like this (I assumed pet has many owners):
public static Specification<Pet> ownerNameEqual(String ownerName) {
return new Specification<Pet>() {
#Override
public Predicate toPredicate(Root<Pet> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
Join<Pet, Owner> owners = root.join("owners");
criteriaQuery.orderBy(criteriaBuilder.desc(root.get("id")));
return criteriaBuilder.equal(owners.get("name"), ownerName);
}
};
}
This is just an example to search all pets whose at least one owner has name equal to ownerName
But you could add a method List<Pet> findByOwnersNameOrderByIdDesc(String ownerName); in your repository instead (as an equivalent to Specification).

JPA CriteriaAPI: correct usage of isMember()

I am using JPAs criteria api to request some objects form my database and apply filter to the request.
the following code shows the method that assambles and executes the query:
#Override
public List<MyClass> findAll(MyClassFilterObject filter) {
final CriteriaBuilder builder = this.getEntityManager().getCriteriaBuilder();
final CriteriaQuery<MyClass> criteriaQuery = builder.createQuery(MyClass);
Root<MyClass> root = criteriaQuery.from(MyClass);
List<Predicate> criteriaList = new LinkedList<Predicate>();
if (!filter.getMyClassStatus().equals("")) {
Path<String> statusPath = root.get("MyClassStatus");
criteriaList.add(builder.equal(statusPath, MyClassStatus.valueOf(filter.getMyClassStatus())));
}
if (!filter.getMyClassType().equals("")) {
Expression<Set<MyClassType>> MyClassTypesQuery = root.get("MyClassType");
criteriaList.add(builder.isMember(MyClassType.valueOf(filter.getMyClassType()), MyClassTypesQuery));
}
if (criteriaList.size()>0){
criteriaQuery.where(builder.and(criteriaList.toArray(new Predicate[criteriaList.size()])));
}
TypedQuery<MyClass> typedQuery = this.getEntityManager().createQuery(criteriaQuery);
typedQuery.setFirstResult(filter.getPagenumber() * 10);
typedQuery.setMaxResults(10);
List<MyClass> MyClasss = typedQuery.getResultList();
if (MyClasss.isEmpty()) {
return new LinkedList<MyClass>();
}
return MyClasss;
}
this code always returns the same two objects, no matter what i choose as "MyClassType".
MyClass has a set of MyClassType (an Enum value) objects attached and i want to know if the MyClassType object from my filter is a member of this collection
I am not sure if i am using the isMember method right

How to count the number of rows of a JPA 2 CriteriaQuery in a generic JPA DAO?

I'm new in JPA and want to implement a generic JPA DAO and need to find the number of rows of a query result set to implement pagination. After searching the web, I can't find a practical way to do that. Here is the code suggested in many articles:
public <T> Long findCountByCriteria(CriteriaQuery<?> criteria) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Long> countCriteria = builder.createQuery(Long.class);
Root<?> entityRoot = countCriteria.from(criteria.getResultType());
countCriteria.select(builder.count(entityRoot));
countCriteria.where(criteria.getRestriction());
return em.createQuery(countCriteria).getSingleResult();
}
However, that code doesn't work when using join. Is there any way to count the rows of a query result set using the JPA Criteria API?
UPDATE :
here is the code that create CriteriaQuery :
CriteriaQuery<T> queryDefinition = criteriaBuilder.createQuery(this.entityClass);
Root<T> root = queryDefinition.from(this.entityClass);
and some joins may be added to the root until the query have been executed:
public Predicate addPredicate(Root<T> root) {
Predicate predicate = getEntityManager().getCriteriaBuilder().ge(root.join(Entity_.someList).get("id"), 13);
return predicate;
}
and the generated exception is like :
org.hibernate.hql.ast.QuerySyntaxException: Invalid path:
'generatedAlias1.id' [select count(generatedAlias0) from entity.Entity
as generatedAlias0 where ( generatedAlias0.id>=13L ) and (
(generatedAlias1.id<=34L ) )]
which generatedAlias1 should be on Entity and generatedAlias0 should be on the association that I joined on that.
Note that I implement Join properly because when I execute query without count query it executes without error and the Join works properly but when I try to execute count query it throws exception.
I've done this:
public Long getRowCount(CriteriaQuery criteriaQuery,CriteriaBuilder criteriaBuilder,Root<?> root){
CriteriaQuery<Long> countCriteria = criteriaBuilder.createQuery(Long.class);
Root<?> entityRoot = countCriteria.from(root.getJavaType());
entityRoot.alias(root.getAlias());
doJoins(root.getJoins(),entityRoot);
countCriteria.select(criteriaBuilder.count(entityRoot));
countCriteria.where(criteriaQuery.getRestriction());
return this.entityManager.createQuery(countCriteria).getSingleResult();
}
private void doJoins(Set<? extends Join<?, ?>> joins,Root<?> root_){
for(Join<?,?> join: joins){
Join<?,?> joined = root_.join(join.getAttribute().getName(),join.getJoinType());
doJoins(join.getJoins(), joined);
}
}
private void doJoins(Set<? extends Join<?, ?>> joins,Join<?,?> root_){
for(Join<?,?> join: joins){
Join<?,?> joined = root_.join(join.getAttribute().getName(),join.getJoinType());
doJoins(join.getJoins(),joined);
}
}
of course you do not need Root as input parameter you could get it from criteria query,
#lubo08 gave correct answer - kudos for him.
But for two corner cases his/her code won't work:
When criteria query's restrictions use aliases for joins - then COUNT also require these aliases to be set.
When criteria query use fetch join [root.fetch(..) instead of root.join(..)]
So for completeness I dared to improve his/her solution and present below:
public <T> long count(final CriteriaBuilder cb, final CriteriaQuery<T> criteria,
Root<T> root) {
CriteriaQuery<Long> query = createCountQuery(cb, criteria, root);
return this.entityManager.createQuery(query).getSingleResult();
}
private <T> CriteriaQuery<Long> createCountQuery(final CriteriaBuilder cb,
final CriteriaQuery<T> criteria, final Root<T> root) {
final CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
final Root<T> countRoot = countQuery.from(criteria.getResultType());
doJoins(root.getJoins(), countRoot);
doJoinsOnFetches(root.getFetches(), countRoot);
countQuery.select(cb.count(countRoot));
countQuery.where(criteria.getRestriction());
countRoot.alias(root.getAlias());
return countQuery.distinct(criteria.isDistinct());
}
#SuppressWarnings("unchecked")
private void doJoinsOnFetches(Set<? extends Fetch<?, ?>> joins, Root<?> root) {
doJoins((Set<? extends Join<?, ?>>) joins, root);
}
private void doJoins(Set<? extends Join<?, ?>> joins, Root<?> root) {
for (Join<?, ?> join : joins) {
Join<?, ?> joined = root.join(join.getAttribute().getName(), join.getJoinType());
joined.alias(join.getAlias());
doJoins(join.getJoins(), joined);
}
}
private void doJoins(Set<? extends Join<?, ?>> joins, Join<?, ?> root) {
for (Join<?, ?> join : joins) {
Join<?, ?> joined = root.join(join.getAttribute().getName(), join.getJoinType());
joined.alias(join.getAlias());
doJoins(join.getJoins(), joined);
}
}
Although it is still not perfect, because only one root is honored.
But I hope it helps somebody.
I can't tell you where your problem is, but I can tell you that count queries with joins work well, at least in eclipselink jpa. My guess is that this is standard stuff, so it should work also in hibernate. I would start by simplifying your code in order to catch where the problem is.
I see that you have copied some pieces of your cont query from your main query. Maybe you can try to change a little bit this approach, just for debugging purpose.
What I do usually is:
CriteriaQuery cqCount = builder.createQuery();
Root<T> root = cq.from(T.class);
cqCount.select(builder.count(root));
ListJoin<T, Entity> join = root.join(T_.someList);
Predicate predicate = builder.ge(join.get(Entity_.id), "myId");
cqCount.where(predicate);
TypedQuery<Long> q = em.createQuery(cqCount);
Looking at your pseudo-code, it seems that you are using the wrong class in the join method: it must be the starting class, not the target class.

Categories