Spring Data JPA: Creating Specification Query Fetch Joins - java

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

Related

Querying a joinned table in JPA Specifications

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!

How to search with optional data and list data in ElasticsearchRepository?

I am using spring-data-elasticsearch and I have a class as follows,
#Document(indexName = "sample")
class Sample {
#Id
private String id;
private String n_id;
private String b_ref;
private String r_ref;
private Status status;
}
Above is persisted to elasticsearch. And I have the following method to find data in it.
List<Sample> getAll(String n_id, Optional<String> b_ref, Optional<String> r_ref, List<Status> statuses);
But the challenge I am facing is I have 2 Optional fields and 1 List field that need to check if contains the status. I tried multiple ways in ElasticsearchRepository but none of them worked.
How can I build the query for the above scenario?
You can implement this using a CriteriaQuery (operations is an ElasticsearchOperations implementation injected in your class):
List<Sample> getAll(String n_id, Optional<String> b_ref, Optional<String> r_ref, List<Status> statuses) {
Criteria criteria = new Criteria("nid").is(n_id).and("status").in(statuses);
if (b_ref.isPresent()) {
criteria = criteria.and("b_ref").is(b_ref.get());
}
if (r_ref.isPresent()) {
criteria = criteria.and("r_ref").is(r_ref.get());
}
SearchHits<Sample> searchHits = operations.search(new CriteriaQuery(criteria), Sample.class);
// to only get the objects without hit information
return searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList());
}

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

Spring Data JPA repository with specification, pagination and criteria fetch-join

I am implementing search/filtering service for list of entities, using Spring Data JPA repository with specifications and pagination features. I am trying to reduce number of queries (n+1 problem) and fetch nested data using criteria fetch mechanism.
I have two entity classes:
#Entity
#Table(name = "delegations")
public class Delegation {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#ManyToOne
private Customer customer;
// more fields, getters, setters, business logic...
}
and
#Entity
#Table(name = "customers")
public class Customer {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
// more fields, getters, setters, business logic...
}
DTO filter class:
public class DelegationFilter {
private String customerName;
// more filters, getters, setters...
}
And search / filtering service:
public class DelegationService {
public Page<Delegation> findAll(DelegationFilter filter, Pageable page) {
Specifications<Delegation> spec = Specifications.where(
customerLike(filter.getCustomerName())
);
return delegationRepository.findAll(spec, page);
}
public List<Delegation> findAll(DelegationFilter filter) {
Specifications<Delegation> spec = Specifications.where(
customerLike(filter.getCustomerName())
);
return delegationRepository.findAll(spec);
}
private Specification<Delegation> customerLike(String customerName) {
return (root, query, cb) -> {
Join<Delegation,Customer> join = (Join) root.fetch(Delegation_.customer);
return cb.like(cb.lower(join.get(Customer_.name)), addWildCards(customerName.toLowerCase()));
};
}
private static String addWildCards(String param) {
return '%' + param + '%';
}
}
Problem:
When I call findAll(DelegationFilter filter, Pageable page) I am getting exception:
org.springframework.dao.InvalidDataAccessApiUsageException:
org.hibernate.QueryException: query specified join fetching, but the owner
of the fetched association was not present in the select list
Is there a way to solve this problem?
findAll(DelegationFilter filter) (method without pagination) works like charm... Using join only (without fetch) also works fine (even with pagination)
I know that there is solution for JPQL:
Spring-Data FETCH JOIN with Paging is not working
But I want to stick with criteria api...
I am using Spring Boot 1.4 (spring 4.3.2, spring-data-jpa 1.10.2) and Hibernate 5.0.9
I was facing the same problem, and I found a workaround (source).
You can check the query's return type at runtime, so that if it is Long (the type the count query returns) you join and otherwise you fetch. In your code it will look like this:
...
private Specification<Delegation> customerLike(String customerName) {
return (root, query, cb) -> {
if (query.getResultType() != Long.class && query.getResultType() != long.class) {
Join<Delegation,Customer> join = (Join) root.fetch(Delegation_.customer);
} else {
Join<Delegation,Customer> join = root.join(Delegation_.customer);
}
return cb.like(cb.lower(join.get(Customer_.name)), addWildCards(customerName.toLowerCase()));
};
}
...
I know it's not very clean, but it's the only solution I've ofund ATM.

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