Multiselect in Spring data Jpa CriteraQuery not giving expected result - java

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.

Related

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.

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 Specification: filter child entities

I successfully implemented a soft delete (aka delete flag) for the entities of my applications. However, I have one remaining problem.
I've written a custom JPARepository with findAll and count methods that filter out the deleted ones. I do this with the specification:
softDeleteSpecification = new Specification<T>() {
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.or(cb.isNull(root.get(DELETED_FIELD_NAME)), cb.equal(root.<T>get(DELETED_FIELD_NAME), false));
}
};
If the entity has for example a OneToMany child list of entities that are also soft deleted, this list is not filtered because the query is not run by its repository.
My question: Can I modify the specification above so that children that are soft deleted are filtered out?
An alternative would be filtering the childs with reflection (manually after the query), but that wouldn't be performant.
With Hibernate, you can use #Whereannotation on your entities
#Entity
#Where(clause = "deleted = 0")
public class MyEntity {
...
That said, take a look at #SQLDelete for an alternative to your implementation of soft delete.
I got this working. In case where each product have one child entity category i.e. Category is child entity to Product, below code worked for me
public static Specification<Product> whereCategoryNameEquals(#NonNull String categoryName) {
return (Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder cb)
-> cb.equal(root.get(Product_.CATEGORY).get(Category_.CATEGORY_NAME),categoryName);
}

Is there a way to reduce the amount of boiler-plate code associated with a CriteriaQuery (in JPA 2.0)?

I love the type safety CriteriaQuery brings ing JPA 2.0 but it also brings a bit of boiler-plate code. For example, let say I have an entity called NamedEntity, which simply has an id and a String field called "name" (assume it has the unique constraint set to true). Here's what the NamedEntityManager might look like:
public class NamedEntityManager
{
//inject using your framework
EntityManager entityManager;
//retrieve all existing entities of type NamedEntity from DB
public Iterable<NamedEntity> queryAll()
{
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<NamedEntity> query = builder.createQuery(NamedEntity.class);
return entityManager.createQuery(query).getResultList();
}
//retrieve a single entity of type NamedEntity from DB using specified name
public NamedEntity queryByName(String name)
{
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<NamedEntity> query = builder.createQuery(NamedEntity.class);
Root<NamedEntity> root = query.from(NamedEntity.class);
query = query.where(root.<NamedEntity>get("name").in(name));
//skipped the try/catch block for the sake of brevity
return entityManager.createQuery(query).getSingleResult();
}
}
Is there a way to condense the code in order to avoid copying/pasting the same lines of code into each query method? Perhaps somehow reuse the CriteriaQuery object?
I was looking for something like that, you could take a look at Querydsl (LGPL licensed) which can have JPA as backend.
Im still reading into it, but from their examples, it looks pretty clean.
HQLQuery q = new HibernateQuery(session);
QCat cat = new QCat("cat"); // query type
List<Cat> cats = q.from(cat).where(cat.name.between("A", "B")).list(cat);
In JPA 2.1, it will most probably be possible to mix JPQL and Criterias. With such an approach you could define a base query with JPQL and then use the Criteria API to dynamically add small parts.
I figure the API will be less verbose then, since you only need to use small parts of it.
Then Use JPA-2.0 MetaData model.
http://docs.jboss.org/hibernate/jpamodelgen/1.0/reference/en-US/html_single/
It seems there's no way to reduce the amount of code. I guess something had to be sacrificed to gain type safety.
Way outdated, this post, but I want to add what I recently built for simple queries
public static class Jpa2Whatsoever {
private final EntityManager em;
public class Jpa2WhatsoeverProgress<T> {
private CriteriaQuery<T> cq;
private List<Predicate> predicates = new ArrayList<>();
private Root<T> root;
public Jpa2WhatsoeverProgress(Class<T> type) {
this.cq = em.getCriteriaBuilder().createQuery(type);
this.root = cq.from(type);
}
public Jpa2WhatsoeverProgress<T> where(String attributeName, Object value) {
Predicate equal = em.getCriteriaBuilder().equal(root.get(attributeName), value);
predicates.add(equal);
return this;
}
public List<T> getResultList() {
Predicate[] predicatesArray = new Predicate[predicates.size()];
TypedQuery<T> typedQuery = em.createQuery(cq.select(root).where(predicates.toArray(predicatesArray)));
List<T> resultList = typedQuery.getResultList();
return Collections.unmodifiableList(resultList);
}
}
public Jpa2Whatsoever(EntityManager entityManager) {
this.em = entityManager;
}
public <T> Jpa2WhatsoeverProgress<T> select(Class<T> type) {
return new Jpa2WhatsoeverProgress<T>(type);
}
}
You can use it like this
List<MyEntity> matchingEntities = new Jpa2Whatsoever(entityManager).select(MyEntity.class).where("id", id).where("due", new Date()).getResultList();
In the end I stopped this. Mainly because I saw that I had only two queries and I would have to extend the DSL to get the required query characteristics into it, such as
greater than, less than
Metamodel support
QueryBuilder.currentDate() and alike.
Further, I find it ugly to always call where while it actually corresponds to a more SQLly and. Anyway, if someone is interested in a very simple query API, it is still worth a try.
BTW: Forget about the names, this was a prototype, nothing more.

Categories