3

I have two tables - user and booking. Each user may have many bookings (one-to-many relationship).

user:                          booking:

    id | name    |               id | country    | user_id | price   |
    -------------|               ------------------------------------|
     1 | Alice   |                1 | Italy      | 1       | 2000    |
     2 | Bob     |                2 | France     | 1       | 2500    |
                                  3 | Spain      | 1       | 3000    |

I want to select all users and all bookings where booking's price is greater than 2000 using Query DSL. If a user doesn't have any bookings or bookings don't match the condition I still want to select this user.

First, let's have a look at how it would look like using a simple SQL left join query:

SELECT u.*, b.* FROM user u LEFT JOIN booking b ON u.id = b.user_id AND b.price > 2000

The above query should provide the following result:

    id | name    | id    | country    | user_id | price   |
    -------------|----------------------------------------|
     1 | Alice   | 2     | France     | 1       | 2500    |
     1 | Alice   | 3     | Spain      | 1       | 3000    |
     2 | Bob     | null  | null       | null    | null    |

Now I want to do it using JPA with Query DSL

JPA-related stuff:

@Entity
public class User {

    @Id
    private Long id;
    private String name;

    @OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
    private List<Booking> bookings;

    // getters and setters
}

@Entity
public class Booking {

    @Id
    private Long id;
    private String name;
    private Integer price;

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    // getters and setters
}

Query DSL:

public List<User> getUsersAndBookings() {
    QUser user = QUser.user;
    QBooking booking = QBooking.booking;

    JPAQuery<User> jpaQuery = new JPAQuery(entityManager);

    List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).on(booking.price.gt(2000)).fetchJoin().fetch();

    return result;
}

In fact, this code is not working and I get the following exception:

org.hibernate.hql.internal.ast.QuerySyntaxException: with-clause not allowed on fetched associations; use filters [select user from com.example.demo.entity.User user left join fetch user.bookings as booking with booking.price > ?1]

The problem is that the condition clause is specified in on method - on(booking.price.gt(2000)).

After some research I found that this condition should be specified in where method and should look like this:

List<User> result = jpaQuery.from(user).leftJoin(user.bookings, booking).where(booking.price.gt(2000)).fetchJoin().fetch();

This works, but not how I would expect it to work, since it doesn't return ALL users, it returns only one user (Alice), which has some bookings, matching the condition clause. Basically, it just filters the merged table (result table after left join operation) and that's not what I'm looking for.

I want to retrieve all users, and if there are no any bookings for a specific user, then just have null instead of booking list for this user.

Please help, been struggling for hours without any success.


Versions used:
  • Spring Boot 2.0.2
  • Spring Data JPA 2.0.7
  • Hibernate 5.2.16.Final
  • QueryDSL 4.1.4
user3127896
  • 6,323
  • 15
  • 39
  • 65
  • Did you get any solution for this? I have this same requirement - need to load the user in result list, even if all his booking not matching the criteria. There I prefer a User object with empty bookings list. The answer by @leopal does not do this. – Ramanujan R Oct 02 '21 at 17:04
  • Got it fixed. See my answer below. https://stackoverflow.com/a/69421978/5035525 – Ramanujan R Oct 03 '21 at 04:00

2 Answers2

1

You can use isNull expression in where clause to get the rows that have null values.

Your query should be like this:

jpaQuery.from(user)
    .leftJoin(user.bookings, booking)
    .fetchJoin()
    .where(booking.price.gt(2000).or(booking.id.isNull())).fetch();

Hibernate produced query:

select
user0_.id as id1_1_0_,
bookings1_.id as id1_0_1_,
user0_.name as name2_1_0_,
bookings1_.country as country2_0_1_,
bookings1_.price as price3_0_1_,
bookings1_.user_id as user_id4_0_1_,
bookings1_.user_id as user_id4_0_0__,
bookings1_.id as id1_0_0__
from
user user0_
left outer join
booking bookings1_
on user0_.id=bookings1_.user_id
where
bookings1_.id is null
or bookings1_.price>?
leopal
  • 4,711
  • 1
  • 25
  • 35
  • 1
    Hi, @leopal. Thank you for your response, but it still doesn't work as left join supposed to work. Let's consider my example again. Suppose that I want to fetch all users and their bookings where the price is greater than 5000 (no bookings exist matching this condition). I expect to retrieve ALL users - Alice and Bob, but your code returns only Alice, since she's got no any bookings at all. I created this sql fiddle to show what I'd like to retrieve - http://www.sqlfiddle.com/#!9/d0a182/2. The main idea is that I always want to retrieve all rows from the left table which is - user. – user3127896 Jun 28 '18 at 07:57
  • Have you tried this? `jpaQuery.from(user).leftJoin(booking) .on(user.id.eq(booking.user.id).and(booking.price.gt(5000))).fetchJoin().fetch();` Let me know if it has the desired results to update my answer. – leopal Jun 28 '18 at 08:59
  • unfortunately doesn't work. the result of this query is all users with all bookings (price condition is ignored). I see that hibernate has generated 3 different queries, **1st query**: `Hibernate: /* select user from User user left join fetch Booking booking with user.id = booking.user.id and booking.price > ?1 */ select user0_.id as id1_11_, user0_.name as name2_11_ from user user0_ left outer join booking booking1_ on (user0_.id=booking1_.user_id and booking1_.price>?)` – user3127896 Jun 28 '18 at 09:26
  • **2nd query**: `Hibernate: select bookings0_.user_id as user_id4_1_0_, bookings0_.id as id1_1_0_, bookings0_.id as id1_1_1_, bookings0_.name as name2_1_1_, bookings0_.price as price3_1_1_, bookings0_.user_id as user_id4_1_1_ from booking bookings0_ where bookings0_.user_id=?` and **3rd query**: `Hibernate: select bookings0_.user_id as user_id4_1_0_, bookings0_.id as id1_1_0_, bookings0_.id as id1_1_1_, bookings0_.name as name2_1_1_, bookings0_.price as price3_1_1_, bookings0_.user_id as user_id4_1_1_ from booking bookings0_ where bookings0_.user_id=?` – user3127896 Jun 28 '18 at 09:27
  • I guess the first query looks fine, but I don't understand why does it generate two more queries, maybe that's the problem – user3127896 Jun 28 '18 at 09:29
  • The 2nd and 3rd queries are performed cause you have requested `Eager` fetch type for the list of the user's bookings(User class). If you use fetch type `Lazy` then these queries will be executed only when you explicitly call `user.getBookings` method. – leopal Jun 28 '18 at 10:04
  • Well if I am not missing something, you want to get from the query a `List of User` and for each user when you call `user.getBookings()` you wait to get the filtered bookings for this user(e.x price>2000). I think thi is not feasible with jpa. In order to achieve your initial goal, in the query I would `select` both user,booking and store this result to a Tuple. Then you can use this tuple to create a list of Users with the booking contained in the tuple(it will contain the filtered bookings e.x price>2000). – leopal Jun 28 '18 at 13:03
  • Yes, that's exactly what I want to do. Why is it not feasible with JPA? The query seems to be pretty trivial, I just can't understand why it is difficult to implement it in JPA. Saying Tuple, do you mean Query DSL's Tuple interface? – user3127896 Jun 29 '18 at 06:44
  • Yes I mean Query DSL's Tuple interface. Of course you can do it with java by filtering the user booking but I think it is not efficient. You can change your query to `select(user,booking)` and fetch a Tuple. After this each row of the tuple will contain a User and the respective Booking(the filtered one). Don't forget that there will be null bookings. As for the reason why it is not feasible by default check this [answer](https://stackoverflow.com/questions/13306235/jpa-one-to-many-filtering) – leopal Jun 29 '18 at 07:19
  • @leopal In general, your suggestion would not give the save results. The ON condition works at the moment when the two records are actually joined, so if it joins A and B, then it can leave the B side NULL, even if the B was not actually NULL (existed, but filtered out). As for the WHERE condition, it filters only the result set, so in our case it would filter out the whole record, whith A and B already joined. – afrish Jan 29 '19 at 06:35
0

It seems there is no JPA way for this. But I got it fixed in Hibernate way, using Filters org.hibernate.annotations.Filter.

@Entity
@FilterDef(name = "anyName", parameters = {
    @ParamDef(name = "price", type = "integer")
})
public class User {    
    @Id
    private Long id;
    private String name;

    @OneToMany(cascade = ALL, fetch = EAGER, orphanRemoval = true, mappedBy = "user")
    @Filter(name = "anyName", condition = "price > :inputPrice")
    private List<Booking> bookings;
}

Before querying the db, you must enable this filter.

Session session = enityManager.unwrap(Session.class);
session.enableFilter("anyName").setParameter("inputPrice", 2000);

// fetch using hql or criteria; but don't use booking.price.gt(2000) or similar condition there

session.disableFilter("anyName");

Now the result will have a User even if all of his booking prices are below 2000 and bookings list will be empty as expected.

NOTE: The word price in condition should be exactly same as the db column name; not as the model property name.

Ramanujan R
  • 1,601
  • 2
  • 23
  • 43