Left Join in Spring Data JPA's Specification - java

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.

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!

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.

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

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.

Categories

Resources