3

I'm using Glassfish 4.1 and JPA 2.1 powered by EclipseLink + Postgresql 9.4.1.

Let's assume we have a car rental company. A customer can rent a car, but the customer can rent the same car only once. Now the goal is to return a list of all cars. However, for each car in the list we want to tell the user whether the user ever rented this car before of not. This additional information (for the UI) can be either a (transient?) boolean flag. In our case, I guess simply filling a corresponding association with the right data fits exactly what we want (see code below). However, I am not very sure how to use a flag instead - any advice here? Anyway... We have to use the Criteria API, as there are many other dynamic filters which we need (irrelevant for this question), so using a NamedQuery with JPQL or even a NamedNativeQuery is not possible and not in our favor.

In other words:

  • The list of cars should contain all available cars
  • Each car in the list ever rented by user 123456 should also have the corresponding rental (the length of this list would always be one then)
  • The Criteria API should generate exactly 1 native SQL query which uses the correct JOIN conditions
  • The association "rentals" for each car should be either empty or filled with exactly one Rental instance of the given user
  • Instead of the given association it would be possibe to use a boolean flag instead, i.e. "alreadyRented" - any idea?

I know how to do this outside of JPA directly on the DB. But I want to use JPA for this. Any I want JPA to fill the association automatically using a single SELECT + LEFT JOIN query, however, things are not not as easy as I thought... Any idea? Would you suggest a different data model?

Here is our Car Entity:

@Entity
public class Car {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column(nullable=false)
    @NotNull
    private String manufacturer;         //simplified

    @OneToMany(mappedBy="car", fetch=FetchType.LAZY)
    private List<Rental> rentals;

    //...
}

According to this mapping, the "rentals" attribute holds a list of all rentals ever made for a given car. Please note that this list is not per user!

And here is the Rental Entity, which basically holds data for all rentals for a given car (again, this is simplified).

@Entity
@Table(
    name="RENTALS",
    uniqueConstraints={
        @UniqueConstraint(columnNames={"CUSTOMER_ID", "CAR_ID"})
    }
)
public class Rental {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional=false, fetch= FetchType.EAGER)
    @JoinColumn(name="CUSTOMER_ID", nullable=false, updatable=false)
    @NotNull
    private Customer customer;

    @ManyToOne(optional=false, fetch=FetchType.EAGER)
    @JoinColumn(name="CAR_ID", nullable=false, updatable=false)
    @NotNull
    private Car car;

    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    @Column(nullable=false)
    @NotNull
    private Date fromDate;

    @Temporal(javax.persistence.TemporalType.TIMESTAMP)
    @Column(nullable=false)
    @NotNull
    private Date toDate;

    //...
}

And here is finally the Customer Entity, which is used in our Rental Entity:

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column(nullable=false)
    @NotNull
    private String firstName;

    @Column(nullable=false)
    @NotNull
    private String lastName;

    //...
}

And here is finally my EJB, which uses the injected EntityManager to access the DB:

@Stateless
@Local
public class CarBean {

    @PersistenceContext(unitName = "myPU")
    private EntityManager em;

    //...

    public List<Car> getCarsForCustomer(Long userId) {

        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Car> q = cb.createQuery(Car.class);
        Root<Car> rootCar = q.from(Car.class);

        List<Predicate> predicates = new ArrayList<Predicate>();
        //...

        //can't just do this because we need a different/dynamic JOIN condition!!
        //rootCar.fetch("rentals", JoinType.LEFT);

        //now let's try to create the dynamic join condition:
        Predicate criteria = cb.conjunction();       

        Join<Car,Rental> rental = rootCar.join("rentals", JoinType.LEFT);
        criteria = cb.and(criteria, cb.equal(rental.get("customer").get("id"), userId) );
        rental.on(criteria);

        q.select(rootCar).where(predicates.toArray(new Predicate[]{}));

        return em.createQuery(q).getResultList();

    }

}

All this will generate the following native SQL statement:

SELECT t1.ID, t1.MANUFACTURER 
    FROM CAR t1 
    LEFT OUTER JOIN RENTALS t0 
        ON ((t0.CAR_ID = t1.ID) AND (t0.CUSTOMER_ID = 123456))

As you can see from the generated statement the joined RENTALS are not part of the result set. Even if it would be part of the result set I'm not sure if JPA would use them to fill the rentals association.

Using a Fetch Join is not possible, as we cannot dynamically choose the join columns/conditions. However, when I uncomment the Fetch Join (see code) then I get the following native SQL statement that uses two JOINS which I don't want:

SELECT 
    t1.ID, t1.MANUFACTURER, t0.ID, t0.FROMDATE, t0.TODATE, t0.CAR_ID, t0.CUSTOMER_ID 
    FROM CAR t1 
        LEFT OUTER JOIN RENTALS t0 ON (t0.CAR_ID = t1.ID) 
        LEFT OUTER JOIN RENTALS t2 ON ((t2.CAR_ID = t1.ID) AND (t2.CUSTOMER_ID = 123456))

So the big question is how can I fill the rentals association by using "dynamic" join conditions? What am I doing wrong?

Nabi
  • 2,536
  • 1
  • 16
  • 18
  • Possible duplicate of [CriteriaBuilder join two tables with a custom condition](https://stackoverflow.com/questions/36406390/criteriabuilder-join-two-tables-with-a-custom-condition) – Peter Šály Mar 21 '19 at 21:49

0 Answers0