Querying a joinned table in JPA Specifications - java

I want to make a request to the endpoint /users?numberOfBooksGreatherThan=5, i want the result to be a list of users with more than 5 books each.
I use the specification API to implements it.
This is what i tried:
public static Specification<User> userNumberOfBooksGreaterThan(Long number){
return ((root, query, criteriaBuilder) -> {
Join<User, Book> userBooks = root.join("userBooksList");
return criteriaBuilder.greaterThan(criteriaBuilder.count(userBooks.get("owner")), number);
});
}
But i got this error
org.postgresql.util.PSQLException: ERROR: aggregate functions are not allowed in WHERE
This is the SQL query executed behind (from logs):
select b1_0.user_id,b1_0.name from users b1_0 join books b2_0 on b1_0.user_id=b2_0.owner_id where count(b2_0.owner_id)>5
i saw some answer to a problem like this but it was just in SQL, saying that i should change where by having but i don't know how to do that with JPA Specification.
when i use the query below (left outer join + group by + having), it returns the correct result, but i don't know how to transform this query into a jpa specification.
select b1_0.user_id,b1_0.name, from users b1_0 left outer join books b2_0 on b1_0.user_id=b2_0.owner_id group by user_id having count(b2_0.owner_id)>=5;
This is the User entity definition
#Entity(name = "users")
public class User {
#Id
private Long userId;
private String name;
#JsonManagedReference
#OneToMany(mappedBy = "owner")
private List<Book> userBooksList;
}
This is the Book entity definition
#Entity(name = "books")
public class Book {
#Id
private Long bookId;
#ManyToOne
#JoinColumn(name = "ownerId",referencedColumnName = "userId",nullable = false)
private User owner;
private Float price;
}
Thank you for your help

I'm not sure the SQL statement you are trying to get:
select b1_0.user_id, b1_0.name from users b1_0
left outer join books b2_0 on b1_0.user_id=b2_0.owner_id
group by user_id
having count(b2_0.owner_id)>=5;
is syntactically correct, at least for Oracle it is not. However there are following options:
Having/group by
(root, cq, cb) -> {
Join<User, Book> books = root.join("userBooksList");
cq.having(cb.greaterThanOrEqualTo(cb.count(books), number));
cq.groupBy(root);
return cb.and();
}
or
(root, cq, cb) -> {
Root<Book> bookRoot = cq.from(Book.class);
cq.having(cb.greaterThanOrEqualTo(cb.count(bookRoot), number));
cq.groupBy(root);
return cb.equal(bookRoot.get("owner"), root);
}
correlated subquery (this one produces syntactically correct SQL statement)
(root, cq, cb) -> {
Subquery<Long> subq = cq.subquery(Long.class);
Root<Book> bookRoot = subq.from(Book.class);
subq.where(cb.equal(bookRoot.get("owner"), subq.correlate(root)));
return cb.greaterThanOrEqualTo(subq.select(cb.count(bookRoot)), number);
}

I think you have a typo (Should be greaterThan instead of greatherThan).
EDIT: You should reverse the query, instead of Join<Book,User> it should be Join<User,Book>. owner is a field of User entity, not reachable through Book.
public static Specification<User> userNumberOfBooksGreaterThan(Long number) {
return (root, query, criteriaBuilder) -> {
Join<User, Book> userBooks = root.join("userBooksList");
return criteriaBuilder.greaterThan(criteriaBuilder.count(userBooks), number);
};}
Changing the comparison in the criteriaBuilder will give you the correct count of books for each user.
Greetings!

Related

Order result set based on the size of collection in an entity

Consider the entities below -
class Team {
private String name;
#OneToMany
private Set<Employee> employees;
}
class Employee {
private String name;
#OneToMany
private Set<Skill> skills;
}
class Skill {
private String name;
private boolean active;
private Date expiryDate;
}
I need to order the Teams resultset such that the team with maximum active & unexpired skills comes first. I am using spring boot Specification & CriteriaQuery to filter Teams in different fields. So far I have the code below which doesn't work as expected.
public class TeamSpecs implements Specification<Team> {
#Override
public Predicate toPredicate(Root<Team> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
Order o = cb.desc(cb.sum(cb.size(root.join("employees").get("skills")));
cq.orderBy(o));
return cb.like(cb.equal(root.get("name"), "%" + value + "%"));
}
}
Anything I am missing here? please suggest
For this to work you first have to join your tables, then filter the entries, group them together and then sort them.
So your SQL query should look like this:
select team.*
from Team team
inner join employee
inner join skill
where skill.active = true and skill.expiryDate > today
group by team.name
order by count(skill.name) desc
Sidenote:
Using a Specification in this case is not what you want to do, because they do not represent a complete Query, but a statement or part of a query, that is used in multiple queries.
Using JPA criteriaquery:
public List<Team> getTeamsWithActiveSkills() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Team> cq = cb.createQuery(Team.class);
Root<Team> root = cq.from(Team.class);
Join<Team, Employee> employees = root.join("employees");
Join<Team, Skill> skills = employees.join("skills");
Predicate isActive = cb.isTrue(skills.get("active"));
Predicate isNonExpired = cb.greaterThan(skills.get("expiryDate"), LocalDate.now());
cq.where(isActive, isNonExpired).groupBy(root.get("name"));
Order order = cb.desc(cb.count(skills));
cq.orderBy(order);
return em.createQuery(cq).getResultList();
}
Since I personally find criteriaquery hard to read and unintuitive you could use Querydsl as an alternative.
Using Querydsl:
public List<Team> getTeamsWithActiveSkills() {
QTeam team = QTeam.team;
QEmployee employee = QEmployee.employee;
QSkill skill = QSkill.skill;
JPQLQuery<Team> query = from(team).join(team.employees, employee).join(employee.skills, skill);
query = teamJPQLQuery.where(skill.active.isTrue().and(skill.expiryDate.gt(LocalDate.now())));
query = query .groupBy(team.name);
query = query .orderBy(skill.name.count().desc());
return query.fetch();
}

Can't make #Formula attributes work with CriteriaBuilder

I have an entity like the following were I use #Formula to populate clientId from other tables.
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Formula("(SELECT cl.id from failure f " +
"INNER JOIN pvplant p ON f.pv_plant_id = p.id " +
"INNER JOIN company co ON p.company_id = co.id "+
"INNER JOIN client cl ON co.client_id = cl.id "+
"WHERE f.id = id) ")
public Integer clientId;
}
while CrudRepository<Failure,Integer> JPA method getByClientId works fine I am trying to make something more dynamic for filtering using a Map of keys and values with Specification and CriteriaBuilder.
public MySpecifications {
public static Specification<Failure> equalToEachColumn(HashMap<String,Object> map) {
return new Specification<Failure>() {
#Override
public Predicate toPredicate(Root<Failure> root, CriteriaQuery<?> cq, CriteriaBuilder builder) {
return builder.and(root.getModel().getAttributes().stream().map(a ->
{
if (map.containsKey(a.getName())) {
Object val = map.get(a.getName());
return builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString()));
}
return builder.disjunction();
}
).toArray(Predicate[]::new)
);
}
};
}
}
When I am passing id in the HashMap it works fine but when I have clientId it doesn't send anything back. It is interesting that getAttributes() actually returns clientId but it seems that builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString())) is false and not true
This is how I am using the Specification:
failureRepository.findAll(Specifications.where(MySpecifications.equalToEachColumn(map)));
Am I missing something?
Thanks in advance!
I wouldn't expect this to work however you could make it work by using a database view as an alternative to #Formula and mapping the entity across the table and view using #SecondaryTable.
//failures_client_vw is a 2 column db view: failure_id, client_id
#Table(name = "failures")
#SecondaryTable(name = "failures_client_vw",
pkJoinColumns = #PrimaryKeyJoinColumn(name = "failure_id"))
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Column(name = "client_id", table = "failures_client_vw")
public Integer clientId;
}
You can then query clientId as you would any other property.
https://en.wikibooks.org/wiki/Java_Persistence/Tables#Multiple_tables
The actual problem was that I was using
builder.disjunction()
in the and() which creates 0=1 false predicates.
When I replaced it with
builder.conjunction() (which creates 1=1 true predicates)
in my code it worked fine. So #Formula properties behave as native ones to the table and it seems there is no need for SecondaryTable and a new View. Apparently in my earlier tests I used an entity that had just an id in its class and when I added clientId it misled me to believe that #Formula properties don't work, while it was the disjunction from the id that broke clientId

Left Join in Spring Data JPA's Specification

Assume I'm having the following class: (simplified to the extreme)
#Entity
#Table(name = "USER")
public class User {
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private BillingAddress billingAddress;
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
private ShippingAddress shippingAddress; // This one CAN be null
}
and both *Address inherit from this abstract: (again, it's extra-simplified)
public abstract class Address {
#OneToOne(optional = false, fetch = FetchType.LAZY)
#JoinColumn(name = "USER_ID")
private User user;
#NotEmpty
#Size(max = 32)
#Column(name = "ADDR_TOWN")
private String town;
}
I tried the JPA Specifications, as explained by Spring's blog post:
/**
* User specifications.
*
* #see Advanced Spring Data JPA - Specifications and Querydsl
*/
public class UserSpecifications {
public static Specification<User> likeTown(String town) {
return new Specification<User>() {
#Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.like(cb.lower(root.get("billingAddress").get("town")), '%' + StringUtils.lowerCase(town) + '%');
}
};
}
Using this "specification" as follow:
List<User> users = userRepository.findAll(UserSpecifications.likeTown(myTown));
But now, I also want to search the town for the shippingAddress, which might not exist.
I tried combining both cb.like in a cb.or but it turned out the resulting SQL query had an INNER JOIN for the shippingAddress, which is incorrect because, as said above, it might be null, so I'd like a LEFT JOIN.
How to do that?
Thanks.
Specify join type:
town = '%' + StringUtils.lowerCase(town) + '%';
return cb.or(
cb.like(cb.lower(root.join("billingAddress", JoinType.LEFT).get("town")), town),
cb.like(cb.lower(root.join("shippingAddress", JoinType.LEFT).get("town")), town));
Don't know if it helps.
I had the same problem. The only way I could solve it was to use a subquery.
For instance this would resemble something like that :
JPASubQuery subquery = new JPASubQuery();
subquery = subquery .from( /* tableB */);
subquery .where(/* conditions */);
Then use i add the subquery to the predicate :
predicate.and(subquery.exists());
NB : In my case it helped as i am extensively using Specifications. In most cases, the performance impact didn't seem that great.
EDIT :
I just realized that the former example worked only in my case as i'm using query-dsl.
In your case, have a look at JPA 2.0, Criteria API, Subqueries, In Expressions to create a subquery and join it to your predicate conditions.

Spring Data JPA: Creating Specification Query Fetch Joins

TL;DR: How do you replicate JPQL Join-Fetch operations using specifications in Spring Data JPA?
I am trying to build a class that will handle dynamic query building for JPA entities using Spring Data JPA. To do this, I am defining a number of methods that create Predicate objects (such as is suggested in the Spring Data JPA docs and elsewhere), and then chaining them when the appropriate query parameter is submitted. Some of my entities have one-to-many relationships with other entities that help describe them, which are eagerly fetched when queried and coalesced into collections or maps for DTO creation. A simplified example:
#Entity
public class Gene {
#Id
#Column(name="entrez_gene_id")
privateLong id;
#Column(name="gene_symbol")
private String symbol;
#Column(name="species")
private String species;
#OneToMany(mappedBy="gene", fetch=FetchType.EAGER)
private Set<GeneSymbolAlias> aliases;
#OneToMany(mappedBy="gene", fetch=FetchType.EAGER)
private Set<GeneAttributes> attributes;
// etc...
}
#Entity
public class GeneSymbolAlias {
#Id
#Column(name = "alias_id")
private Long id;
#Column(name="gene_symbol")
private String symbol;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="entrez_gene_id")
private Gene gene;
// etc...
}
Query string parameters are passed from the Controller class to the Service class as key-value pairs, where they are processed and assembled into Predicates:
#Service
public class GeneService {
#Autowired private GeneRepository repository;
#Autowired private GeneSpecificationBuilder builder;
public List<Gene> findGenes(Map<String,Object> params){
return repository.findAll(builder.getSpecifications(params));
}
//etc...
}
#Component
public class GeneSpecificationBuilder {
public Specifications<Gene> getSpecifications(Map<String,Object> params){
Specifications<Gene> = null;
for (Map.Entry param: params.entrySet()){
Specification<Gene> specification = null;
if (param.getKey().equals("symbol")){
specification = symbolEquals((String) param.getValue());
} else if (param.getKey().equals("species")){
specification = speciesEquals((String) param.getValue());
} //etc
if (specification != null){
if (specifications == null){
specifications = Specifications.where(specification);
} else {
specifications.and(specification);
}
}
}
return specifications;
}
private Specification<Gene> symbolEquals(String symbol){
return new Specification<Gene>(){
#Override public Predicate toPredicate(Root<Gene> root, CriteriaQuery<?> query, CriteriaBuilder builder){
return builder.equal(root.get("symbol"), symbol);
}
};
}
// etc...
}
In this example, every time I want to retrieve a Gene record, I also want its associated GeneAttribute and GeneSymbolAlias records. This all works as expected, and a request for a single Gene will fire off 3 queries: one each to the Gene, GeneAttribute, and GeneSymbolAlias tables.
The problem is that there is no reason that 3 queries need to run to get a single Gene entity with embedded attributes and aliases. This can be done in plain SQL, and it can be done with a JPQL query in my Spring Data JPA repository:
#Query(value = "select g from Gene g left join fetch g.attributes join fetch g.aliases where g.symbol = ?1 order by g.entrezGeneId")
List<Gene> findBySymbol(String symbol);
How can I replicate this fetching strategy using Specifications? I found this question here, but it only seems to make lazy fetches into eager fetches.
Specification class:
public class MatchAllWithSymbol extends Specification<Gene> {
private String symbol;
public CustomSpec (String symbol) {
this.symbol = symbol;
}
#Override
public Predicate toPredicate(Root<Gene> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
//This part allow to use this specification in pageable queries
//but you must be aware that the results will be paged in
//application memory!
Class clazz = query.getResultType();
if (clazz.equals(Long.class) || clazz.equals(long.class))
return null;
//building the desired query
root.fetch("aliases", JoinType.LEFT);
root.fetch("attributes", JoinType.LEFT);
query.distinct(true);
query.orderBy(cb.asc(root.get("entrezGeneId")));
return cb.equal(root.get("symbol"), symbol);
}
}
Usage:
List<Gene> list = GeneRepository.findAll(new MatchAllWithSymbol("Symbol"));
You can specify the join fetch while creating Specification but since the same specification will be used by pageable methods also
like findAll(Specification var1, Pageable var2) and count query will complain because of join fetch. Therefore, to handle that we can check the resultType of CriteriaQuery and apply join only if it is not Long (result type for count query). see below code:
public static Specification<Item> findByCustomer(Customer customer) {
return (root, criteriaQuery, criteriaBuilder) -> {
/*
Join fetch should be applied only for query to fetch the "data", not for "count" query to do pagination.
Handled this by checking the criteriaQuery.getResultType(), if it's long that means query is
for count so not appending join fetch else append it.
*/
if (Long.class != criteriaQuery.getResultType()) {
root.fetch(Person_.itemInfo.getName(), JoinType.LEFT);
}
return criteriaBuilder.equal(root.get(Person_.customer), customer);
};
}

JPA Criteria Query with IN operator for Set<?> with Spring DATA-JPA

In my application, I have the following mapping between two entities :
#Entity
public class Applicant {
private Integer id;
....
private Set<Document> documents;
... Getters and Setters ...
#OneToMany(fetch = FetchType.LAZY, mappedBy = "applicant", cascade = CascadeType.ALL)
public Set<Document> getDocuments() {
return documents;
}
public Applicant setDocuments(Set<Document> documents) {
this.documents = documents;
return this;
}
}
And Document :
public class Document {
private Long id;
private Applicant applicant;
... Getters and Setters ...
#ManyToOne
public Applicant getApplicant() {
return applicant;
}
public Document setApplicant(Applicant applicant) {
this.applicant = applicant;
return this;
}
}
I want to use the Spring Data Specification (org.springframework.data.jpa.domain) to filter some applicant in my ApplicantRepository with the findAll(Spec spec) method.
But, my problem is I want to create a specification witch take in parameters a Set and build a specification to filter the applicant who are not linked to one (not all) of this document.
I've tried different things but none of them work... I don't know if I am forgetting something.
The first one was to use the criteriaBuilder and value method...
public static Specification<Applicant> applicantHasDoc(final Set<Document> documents) {
return new Specification<Applicant>() {
#Override
public Predicate toPredicate(Root<Applicant> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
/*
Problem during parsing the query :
select *
from
applicant applicant0_ cross join document documents1_
where
applicant0_.id=documents1_.applicant
and (. in (? , ?))
*/
Expression<Set<Document>> documentExpression = root.get(Applicant_.documents);
return cb.in(documentExpression).value(documents);
};
}
That's returning an GrammarSQL exception... you can see the SQL query (simplified on the applicant fields) in the code.
The second solution was to use metamodel and In directly on the ROOT of applicant :
public static Specification<Applicant> applicantHasDoc(final Set<Document> documents) {
return new Specification<Applicant>() {
#Override
public Predicate toPredicate(Root<Applicant> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
/*
Error with this type of criteria : Parameter value [com.myapp.entity.Document#1b275eae] did not match expected type [java.util.Set (n/a)]
*/
return root.get(Applicant_.documents).in(documents);
}
};
}
I have add in the code the result of each solution... and none of them work.
The main purpose of this Specification is to be used with others like that :
List<Applicant> applicants = findAll(where(applicantHasDoc(documents).and(otherSpec(tags)).and(anotherSpec(mobilities), page);
So I can only work inside a Spring Data JPA Specification.
Other information : I'm using H2 Database.
Thanks for your help.
I find the right way to do that, and all my attempt was bad because I was thinking "like object" and not in SQL... but CriteriaQuery is a "object wrapper" to build SQL query.
So, I wrote the query I want in SQL and I found the solution :
What I want in SQL was :
select *
from applicant applicant0_ inner join document documents1_ on applicant0_.id=documents1_.applicant where documents1_.id in (? , ?)
So my predicate seems to be the following :
public static Specification<Applicant> applicantHasDoc(final Set<Document> documents) {
return new Specification<Applicant>() {
#Override
public Predicate toPredicate(Root<Applicant> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
SetJoin<Applicant, Document> documentApplicantJoin = root.join(Applicant_.documents);
return documentApplicantJoin.in(documents);
}
};
}

Categories

Resources