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

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.

Related

Spring Data JPA how to specify Join type or Fetch Mode when using get("property") chain vs Join

I have two (Hibernate-based) Spring Data JPA domain classes, the "One" side Customer.class:
#Entity
#Table(name = "sys_customer")
#Data
public class Customer implements Serializable {
#Id
#Column(name = "cust_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "cust_name")
private String customerName;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
private Set<Order> orders;
}
and the "Many" side Order.class:
#Entity
#Table(name = "sys_order")
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Order implements Serializable {
#Id
#Column(name = "order_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "order_name")
private String orderName;
#ManyToOne
#JoinColumn(name = "order_cust_id", referencedColumnName = "cust_id")
private Customer customer;
public Order( String orderName) {
this.orderName = orderName;
}
public Order(String orderName, Customer customer) {
this.orderName = orderName;
this.customer = customer;
}
}
I have OrderRepository interface which extends JpaRepository interface and JpaSpecificationExecutor interface:
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
I have a OrderSpecification.class with the static method searchByCustomerName:
public class OrderSpecification {
public static Specification<Order> searchByCustomerName(String customerName) {
return new Specification<Order>() {
#Override
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Join<Order, Customer> join = root.join("customer");
return criteriaBuilder.like(join.get("customerName"), "%" + customerName + "%");
//return criteriaBuilder.like(root.get("customer").get("customerName"), "%" + customerName + "%");
}
};
}
}
To find the differences between get("property") chain and Join, I wrote a simple test method and comment out the above OrderSpecificatin.class code
#Test
#Transactional
public void testFindOrderByCustomerName(){
String name = "adam";
List<Order> orders = orderRepository.findAll(OrderSpecification.searchByCustomerName(name));
for(Order order: orders){
Customer customer = order.getCustomer();
log.info(new StringBuilder().append(customer.getId()).append(" ").append(customer.getCustomerName()).toString());
}
}
I found that:
get("property") chain use a cross-join(which is very bad performancing) while Join use inner-join(since ManyToOne() by default is Fetch= FetchType.EAGER)
/* get("property") chain: Hibernate: select order0_.order_id as
order_id1_1_, order0_.order_cust_id as order_cu3_1_,
order0_.order_name as order_na2_1_ from sys_order order0_ cross join
sys_customer customer1_ where order0_.order_cust_id=customer1_.cust_id
and (customer1_.cust_name like ?) Hibernate: select customer0_.cust_id
as cust_id1_0_0_, customer0_.cust_name as cust_nam2_0_0_ from
sys_customer customer0_ where customer0_.cust_id=? */
/** * "Join": * Hibernate: select order0_.order_id as order_id1_1_,
order0_.order_cust_id as order_cu3_1_, order0_.order_name as
order_na2_1_ from sys_order order0_ inner join sys_customer customer1_
on order0_.order_cust_id=customer1_.cust_id where customer1_.cust_name
like ? * Hibernate: select customer0_.cust_id as cust_id1_0_0_,
customer0_.cust_name as cust_nam2_0_0_ from sys_customer customer0_
where customer0_.cust_id=? */
My questions are:
Can I specify the Join type(inner, all three outers) or Fetch Type(LAZY, EAGER) when using get("property") chain approach to avoid cross-join?
What scenario/best practice should I use get("chain") or always stay in Join?
Does the approach OrderSpecification.class with static method obey a good OOP design pattern?
You can't specify the join type for paths. It will use INNER join semantics by default and that is mandated by the JPA specification. If you want a different join type, you will have to create joins explicitly. The fact that using get renders as cross joins is a limitation of the old query model of Hibernate, but Hibernate 6.0 will fix this. The semantics are the same though and the query planner of your database should be able to treat both queries the same way. Maybe you just need to update your database version?
There is no "best practice" i.e. this really depends on your needs. Explicit joins are just that, explicit. So multiple calls to join will create multiple joins in SQL.
As for the OOP question, I think this is fine, yes.

Hibernate Criteria: projection with joined entity

I'm trying to create a query with Criteria, but I don't succeed to map data from a joined entity.
With this Criteria query the id of the Order entity is override with the id of the ShippingCondition entity :
final Criteria criteria = session.createCriteria(Order.class, "o")
.createAlias("o.shippingCondition", "sc", JoinType.INNER_JOIN)
.setProjection(Projections.projectionList()
.add(Projections.property("o.id"), "id")
.add(Projections.property("o.orderNum"), "orderNum")
.add(Projections.property("o.notes"), "notes")
.add(Projections.property("sc.id"), "id"))
.add(Restrictions.eq("o.id", id))
.setResultTransformer(Transformers.aliasToBean(Order.class));
return (Order) criteria.uniqueResult();
My entities :
#Table(name = "order", schema = "myschema")
public class Order {
private Integer id;
private String orderNum;
private String notes;
private ShippingCondition shippingCondition;
...
}
#Table(name = "shipping_condition", schema = "myschema")
public class ShippingCondition {
private Integer id;
private String shippingCondition;
private Integer sorting;
...
}
I have tryed to replace .add(Projections.property("sc.id"), "id") by .add(Projections.property("sc.id"), "shippingCondition.id") but then I get a ClassCastException (java.lang.ClassCastException: entity.Order cannot be cast to java.util.Map)
Do you have any idea how I can do that ?
Thanks
Hibernate doesn't support nested projections. You will need to create DTO for that. You can extend DTO from Order class and add methods to set fields of ShippingCondition.
class OrderDto extends Order {
public OrderDto() {
setShippingCondition(new ShippingCondition());
}
public void setShippingConditionId(Integer id) {
getShippingCondition().setId(id);
}
}
You can use a special nested transformer if you don't want to use DTO
How to transform a flat result set using Hibernate
Additional notes
JPA doesn't support any transformers at all. And it is hard to implement such transformer by consistent way. For example, my transformer doesn't support child collections like #OneToMany, only single associations. Also, you can't use nested projections with HQL, because HQL doesn't support parent.child aliases.

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

Hibernate #Filter does not work in JPA?

I'm using JPA with Hibernate as a JPA provider. I cannot figure out how to configure my entities to apply a hibernate filter to a One-to-Many association.
I have a Master with a collection of Details. Here are my entity definitions:
#Entity
public class Master extends Base {
private List<Detail> details;
#OneToMany
#OrderColumn
#JoinTable(name = "master_details")
#Filter(name = "notDeleted")
// #Where(clause = "deleted = 'false'")
public List<Detail> getDetails() {
return details;
}
public void setDetails(List<Detail> details) {
this.details = details;
}
}
#Entity
#FilterDef(name = "notDeleted", defaultCondition = "deleted = false")
public class Detail extends Base {
private Boolean deleted = false;
public Boolean getDeleted() {
return deleted;
}
public void setDeleted(Boolean deleted) {
this.deleted = deleted;
}
}
The Base is nothing special but a simple MappedSuperClass:
#MappedSuperclass
public class Base {
private Long id;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
When loading a Master by entityManager.find(Master.class, mid), the filter should prvent all Details from loading but I checked the sql queries generated by hibernate (by show_sql=true) and no where clause is added when loading details of the master !!! A sample query generated by hibernate is:
select
details0_.Master_id as Master1_6_1_,
details0_.details_id as details2_1_,
details0_.details_ORDER as details3_1_,
detail1_.id as id7_0_,
detail1_.deleted as deleted7_0_,
from
master_details details0_
inner join
Detail detail1_
on details0_.details_id=detail1_.id
where
details0_.Master_id=?
After some search there was some hints that "loading by id will not use filters, use queries" so I tried the following but no gain :(
entityManager.createQuery("from Master where id=" + mid).getSingleResult();
But just if the #Where above getDetails is uncommented (instead of #Filter), its clause is added to the query generated by hibernate (but I cannot use #Where)
The Hibernate #Filter needs to be manually activated via enableFilter method:
session.enableFilter("myFilter").setParameter("myFilterParam", "some-value");
However, filters are useful when you need to parameterize the filtering condition. And, you don't seem to need a dynamic filtering clause.
For this reason, you could use the Hibernate #Where filter, like this:
#org.hibernate.annotations.Where(clause="deleted=false")
public List<Detail> getDetails() {
return details;
}
This way, you should get the list of non-deleted Detail entities.

Categories

Resources