JpaSpecificationExecutor JOIN + ORDER BY in Specification - java

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

Related

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

Spring - OrderBy relation attribute using CriteriaQuery (Specification) ignores the rows which does not have the relation

I want to order by a relation attribute, I am using toPredicat (Jpa Specification, SpringBoot framework) to generate the query.
I am getting the right order but the problem is that the rows that does not have this relation (the relation is null) is been removed from the result set.
Below is an example:
let's say we have two entity, Email which may have a Document.
I want to order the Emails using the Document.name (the attribute name of Document)
So i have my specification:
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
public class EmailSpecification implements Specification<Email> {
#Override
public Predicate toPredicate(Root<Email> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();
if (xx) {
predicates.add(criteriaBuilder.like(yy);
}
//...
criteriaQuery.orderBy(criteriaBuilder.asc(
root.get('document').get('name'));
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]))
}
}
and when i Call
List<Email> result = emailRepository.findAll(new EmailSpecification());
I have a list of emails with the right order but with only the email that have a document ( I want all the email even if it does not have a document)
I think you should need a Left join for that, maybe something like:
criteriaQuery.orderBy(criteriaBuilder.asc(
root.join("document", JoinType.LEFT).get("name"));

Order by subquery with Spring JPA Specification

I have a problem with sorting, which would have been easily solved by native SQL and probably JPQL, but not Spring JPA Specifications.
Here is the trimmed domain model:
#Entity
class DesignElement {
List<Change> changes;
}
#Entity
class Change {
List<DesignElement> designElements;
}
#Entity
class Vote {
Change change;
VoteType type;
}
Users publish their Change proposals on several elements, and other users vote these changes with VoteType of GREAT, GOOD, NEUTRAL or BAD.
What I try to accomplish is to order Entitys (not Changes) by the count of GOOD and GREAT votes.
It might have been easier with SQL or JPQL but we try to utilize Specification API since we need several other predicates and orders.
After two days of agony I have come up with several assumptions about what I cannot do with Specification api. Some of them are probably wrong, if so I will edit the question with answers.
We cannot just add count(Vote_.id) in the root query if we have several Specification<DesignElement>s to be combined.
It is not possible to order by subquery in JPA: https://hibernate.atlassian.net/browse/HHH-256
It is not possible to do a multiselect in subquery.
Here are several other working Orders defined inside other Specification<DesignElement>s:
private static void appendOrder(CriteriaQuery<?> query, Order order) {
List<Order> orders = new ArrayList<>(query.getOrderList());
orders.add(0, order);
query.orderBy(orders);
}
public static Specification<DesignElement> orderByOpen() {
return new Specification<DesignElement>() {
#Override
public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
appendOrder(query, cb.desc(root.get("status").get("open")));
return null;
}
};
}
public static Specification<DesignElement> orderByReverseSeverityRank() {
return new Specification<DesignElement>() {
#Override
public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
appendOrder(query, cb.desc(root.get("severity").get("rank")));
return null;
}
};
}
public static Specification<DesignElement> orderByCreationTime() {
return new Specification<DesignElement>() {
#Override
public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
appendOrder(query, cb.asc(root.get("creationTime"));
return null;
}
};
}
Which would allow a usage like that:
List<DesignElement> elements = designElementDao.findAll(Specifications.where(orderByReverseSeverityRank()).and(orderByCreationTime());
I was facing same problem. For sorting by count(child-collection) I found a workaround - it's not ideal (polluting entity objects, not dynamic etc.), but for some cases, it might be enough.
define read-only field 'upvotesCount' holding the information to be used for sort
#Entity
class Change {
List<DesignElement> designElements;
// let's define read-only field, mapped to db view
#Column(table = "change_votes_view", name = "upvotes_count", updatable = false, insertable = false)
Integer upvotesCount;
}
implement db view which where the field is mapped to
CREATE VIEW change_votes_view AS SELECT c.id, count(v.*) as upvotes_count
FROM change c
LEFT JOIN votes v
ON <ids-condition> AND <votes-are-up=condition>
Another way would be to go with DTO objects - you construct query which fits your needs and you can do it also dynamically.
I have come up with somewhat hacky solution.
There was another problem which was not apparent in the H2 unit test DB, but showed up on the actual Postgres installation: You have to group by a column if it is directly used inside an Order by clause: must appear in the GROUP BY clause or be used in an aggregate function
The solution involves around two different approaches:
Construct the joins so that there won't be duplicate rows and just do an identity aggregate operation (like sum) on these single rows
Or
Construct the joins with interested duplicate rows and count the number of child id's. (And don't forget to group by root id).
Counting the number of child id's was the way to go for this problem. But even that (creating a precondition on the interested rows) proved difficult (I am pretty sure it is possible) so I took a somewhat hacky approach: cross join all the children, and do a sum on case:
public static Specification<DesignElement> orderByGoodVotes() {
return new Specification<DesignElement>() {
#Override
public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
ListAttribute<? super DesignElement, Alert> designElementChanges = root.getModel().getList("changes", Change.class);
ListJoin<Change, Vote> changeVotes = root.join(designElementChanges).joinList("votes", JoinType.LEFT);
Path<VoteType> voteType = changeVotes.get("type");
// This could have been avoided with a precondition on Change -> Vote join
Expression<Integer> goodVotes1_others0 = cb.<Integer>selectCase().when(cb.in(voteType).value(GOOD).value(GREAT, 1).otherwise(0);
Order order = cb.desc(cb.sum(goodVotes1_others0));
appendOrder(query, order);
// This is required
query.groupBy(root.get("id"));
return null;
}
};
}
I generalized this approach and fixed the previous specifications:
public static Specification<DesignElement> orderByOpen() {
return new Specification<DesignElement>() {
#Override
public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Expression<Integer> opens1_others0 = cb.<Integer>selectCase().when(cb.equal(root.get("status").get("open"), Boolean.TRUE), 1).otherwise(0);
appendOrder(query, cb.desc(cb.sum(opens1_others0)));
query.groupBy(root.get("id"));
return null;
}
};
}
public static Specification<DesignElement> orderByReverseSeverityRank() {
return new Specification<DesignElement>() {
#Override
public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
appendOrder(query, cb.asc(cb.sum(root.get("severity").get("rank"))));
query.groupBy(root.get("id"));
return null;
}
};
}
public static Specification<DesignElement> orderByCreationTime() {
return new Specification<DesignElement>() {
#Override
public Predicate toPredicate(Root<DesignElement> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// This isn't changed, we are already selecting all the root attributes
appendOrder(query, cb.desc(root.get("creationTime")));
return null;
}
};
}
Since there are ~15 other Specifications like these, I guess there will be a performance penalty for being so lazy but I did not analyze it.

Multiselect in Spring data Jpa CriteraQuery not giving expected result

I have one entity as Project and I am writing one jpa specification for implementing pagination. Now the problem is I don't want whole entity,I just want some columns of that entity.
My specification looks like -
public class ProjectSpecification {
public static Specification<Project> projectListSearchSpec(Set<Long> deptIdList, SearchDto searchDTO) {
return new Specification<Project>() {
#Override
public Predicate toPredicate(Root<Project> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
query.multiselect(root.get("id"));
Predicate all = root.<Project>get("department").get("departmentId").in(deptIdList);
return all;
}
};
}
}
As it can be seen in the snippet that I am trying to use multi-select but there is nor effect of that.
I am still getting all the attributes of entity.

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