JPA #OneToMany Fetch Overrides CriteriaQuery Join Condition - java

I am quite new to Hibernate and the Criteria API, and am running into troubles using them.
There are two Entities:
#Entity
public class Product {
#Id
#GeneratedValue
private Integer id;
private String productName;
#OneToMany(fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
private List<ProductPrice> prices = new ArrayList<>();
}
#Entity
public class ProductPrice {
#Id
#GeneratedValue
private Integer id;
private BigDecimal price;
private String region;
private LocalDate startDate;
}
Products have multiple ProductPrices. Each ProductPrice belongs to a Region.
The goal is to query Products and all their historical Prices for a specific Region:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> root = cq.from(Product.class);
ListJoin<Product, ProductPrice> productJoin = root.join(Product_.prices, JoinType.INNER);
productJoin.on(cb.equal(productJoin.get(ProductPrice_.region), "REGION1"));
List<Product> products = em.createQuery(cq.distinct(true)).getResultList();
This generates the following SQL Query:
select
distinct product0_.id as id1_1_,
product0_.productName as productN2_1_
from
Product product0_
inner join
(
Product_ProductPrice prices1_
inner join
ProductPrice productpri2_
on prices1_.prices_id=productpri2_.id
)
on product0_.id=prices1_.Product_id
and (
productpri2_.region=?
)
I tried that query and it seems to work, however as soon as getPrices() is called on one of the Products, the Product's Prices are lazily fetched, like so:
select
prices0_.Product_id as Product_1_2_0_,
prices0_.prices_id as prices_i2_2_0_,
productpri1_.id as id1_3_1_,
productpri1_.price as price2_3_1_,
productpri1_.region as region3_3_1_
from
Product_ProductPrice prices0_
inner join
ProductPrice productpri1_
on prices0_.prices_id=productpri1_.id
where
prices0_.Product_id=?
which makes sense, because of the association #OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL), but in this case, for this specific query, I don't want this behaviour. I did not find an example like that in the Hibernate UserGuide or here on stackoverflow so I guess I am missing something very obvious. But still, I couldn't find a solution to my problem.
Thanks!

As mentioned in the comments above, on specifies the columns needed for the join. In your situation, you need a where.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> cq = cb.createQuery(Product.class);
Root<Product> root = cq.from(Product.class);
ListJoin<Product, ProductPrice> productJoin = root.join(Product_.prices, JoinType.INNER);
cq.where(cb.equal(productJoin.get(ProductPrice_.region), "REGION1"));
List<Product> products = em.createQuery(cq).getResultList();
In addition, you should have a look whether your #OneToMany mapping is designed efficiently like this. This excellent article of Vlad Mihalcea describes how to map a #OneToMany efficiently: Either make it bidirectional or unidirectional with #ManyToOne.
For the issue of your lazy loads, have a look for lazy initialisation. I really like graphs for doing this.

Related

JPA Criteria: QuerySyntaxException for left join on treat entity

Model:
#Entity
public class User {
#Id
private Integer id;
#JoinColumn(name = "user_id")
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Project> projects;
}
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "Type")
public abstract class Project {
#Id
private Integer id;
private String name;
}
#Entity
#DiscriminatorValue("Administrative")
public class AdminProject extends Project {
private String departmentName;
}
#Entity
#DiscriminatorValue("Design")
public class DesignProject extends Project {
private String companyName;
}
I am trying to use JPA's criteria api to query for User entities based on an attribute of an implementation of Project. For example, query all users that have a project with "SOME_NAME" department (that field does not exist on DesignProject).
I see there is a way of doing so via downcasting of the Project entity for the query. I am trying something similar to:
CriteriaBuilder cb...
Root<User> userRoot...
root = ((From) root).join("projects", JoinType.LEFT);
root = cb.treat(root, AdminProject.class);
root = root.get("departmentName");
Exception:
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias2.departmentName' [select generatedAlias0 from io.github.perplexhub.rsql.model.User as generatedAlias0 left join generatedAlias0.projects as generatedAlias1 where treat(generatedAlias2 as io.github.perplexhub.rsql.model.AdminProject).departmentName=:param0]; nested exception is java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias2.departmentName' [select generatedAlias0 from io.github.perplexhub.rsql.model.User as generatedAlias0 left join generatedAlias0.projects as generatedAlias1 where treat(generatedAlias2 as io.github.perplexhub.rsql.model.AdminProject).departmentName=:param0]
What am I missing? Is it something related to the join, or how the downcasting occurs afterwards?
Edit
After the answer by #K.Nicholas, I have managed to make the query work on an isolated scenario, but not on my app.
But, I noticed that the entityManager.createQuery(query) call throws the exception above when called for the first time, and it works if I call it again without changing the query object. Here is the query generated on the second call (this query finds the objects I want from the database):
select generatedAlias0 from User as generatedAlias0 left join generatedAlias0.projects as generatedAlias2 where treat(generatedAlias2 as io.github.perplexhub.rsql.model.AdminProject).departmentName=:param0
Why is the entity manager creating two different queries when called two consecutive times?
I would do the Entitys a little different, as you will see. The main concern is that you are using User as your root with a join to a list of Projects. This is a concern because you should have the foreign key on the Project class and use the projects field as a query only field. That is what I have done. It works better that way. It is also a concern because you have to do a join fetch instead of a join so that the projects get fetched along with the users.
So, first, the entities are like so:
#Entity
public class User {
#Id
private Integer id;
#OneToMany(mappedBy="user")
private List<Project> projects;
}
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "Type")
public abstract class Project {
#Id
private Integer id;
private String name;
#ManyToOne
private User user;
}
#Entity
#DiscriminatorValue("Administrative")
public class AdminProject extends Project {
private String departmentName;
}
#Entity
#DiscriminatorValue("Design")
public class DesignProject extends Project {
private String companyName;
}
After a bit a digging I found a JPQL query that does the trick. This was a starting point:
List<User> users = entityManager.createQuery("select distinct(u) from User u join fetch u.projects p where TYPE(p) = 'Administrative' and p.departmentName = 'dept1'", User.class).getResultList();
After a bit more digging I found that the treat worked fine if you do it correctly and that with JPA 2.1 you should use an EntityGraph do get the join to do a fetch.
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
Root<User> root = query.from(User.class);
Join<User, Project> join = root.join("projects");
query.select(root).where(builder.equal(builder.treat(join, AdminProject.class).get("departmentName"), "dept1"));
EntityGraph<User> fetchGraph = entityManager.createEntityGraph(User.class);
fetchGraph.addSubgraph("projects");
users = entityManager.createQuery(query.distinct(true)).setHint("javax.persistence.loadgraph", fetchGraph).getResultList();
As a side note the queries generated as slightly different but I didn't look that closely at them. You should.

How Can I avoid N+1 issue in JPA with several relationship?

I have the following entities:
#Entity
#Table(name="table1")
public class Entity1 {
#Id
private Integer id;
#OneToMany(mappedBy = "entity1")
private List<Entity2> entities2;
}
#Entity
#Table(name="table2")
public class Entity2 {
#Id
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="id")
private Entity3 entity3;
}
Using Criteria API I have tried:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Entity1> query = cb.createQuery(Entity1.class);
Root<Entity1> entity1= query.from(Entity1.class);
entity1.fetch("entities2", JoinType.LEFT);
entity1.fetch("entities2", JoinType.LEFT).fetch("entity3", JoinType.LEFT);
But when the query is executed:
query.select(entity1).where(cb.and(predicates.toArray(new Predicate[predicates.size()]))));
List<Entity1> entities1 = entityManager.createQuery(query).getResultList();
Multiple queries intead one are executed (related to Entity 3). I think the problem is beacuse the relationship is inside another one. Because when you fecth the first join, there are not several queries.
I would appreciate your help. Thank so much
By default the relationships wich are like Collections the hibernate deal how lazy,
if you defined relationship how Lazy and not received LazyLoadException check your config.

How to add sub-select to select

I want to execute a query like this:
SELECT Table1.COL1,
Table1.COL2,
(SELECT SUM(Table2.COL3)
FROM Table2
WHERE Table2.UID = Table1.UID) SUMOF
FROM Table1;
How can I do it?
I usually create a Criteria add ProjectionList to it, to fill COL1 and COL2 only.
I have created a DetachedCriteria to calculate the sum...
Now, how to attach this detached criteria to the main one? My intuition says - it's some sort of Projection which needs to be added to the list, but I don't see how. Also, not sure how WHERE Table2.COL4 = Table1.COL5 of detached criteria will work.
Also, I'm sure this query might be written in different way, for example with join statement. It's still interesting if there's a way to run it like this.
DetachedCriteria and main Criteria
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Table2.class, "table2");
detachedCriteria
.setProjection(
Projections.projectionList()
.add(Projections.sum("table2.col3"), "sumCol3")
)
.add(Restrictions.eq("table2.uid", "table1.uid"))
;
Criteria criteria = session.createCriteria(Table1.class, "Table1");
criteria
.setProjection(
Projections.projectionList()
.add(Projections.property("Table1.col1"), "col1")
.add(Projections.property("Table1.col2"), "col2")
)
;
Entities (very short version)
#Entity
#Table(name = "Table1")
public class Table1 {
#Id
#Column(name = "uid")
public String getUid();
#Column(name = "col1")
public String getCol1();
#Column(name = "col2")
public String getCol2();
#Column(name = "col3")
public String getCol3();
#Column(name = "col4")
public String getCol4();
#Column(name = "col5")
public String getCol5();
}
#Entity
#Table(name = "Table2")
public class Table2 {
#Id
#Column(name = "uid")
public String getUid();
#Column(name = "col3")
public BigDecimal getCol3();
#Column(name = "col4")
public String getCol4();
#Column(name = "col5")
public String getCol5();
}
For a correlated subquery (like the one you presented above), you can use #Formula which can take an arbitrary SQL query. Then, you'll need to fetch the entity and the subquery will be executed.
However, a native SQL is more elegant if you only need this query for a single business requirement.
As for derived table queries (e.g. select from select), neither JPA nor Hibernate support derived table queries for a very good reason.
Entity queries (JPQL pr Criteria) are meant to fetch entities that you plan to modify.
For a derived table projection, native SQL is the way to go. Otherwise, why do you think EntityManager offers a createNativeQuery method?

JPA 2.1 - How to use dynamic join conditions with the Criteria API to fill OneToMany associations?

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?

Is it possible to define LEFT Join when annotating a relationship with #OneToMany?

I have the following scenario
#Entity
public class Parent {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name = "name", nullable = false)
private String name;
#OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL, mappedBy = "parent", orphanRemoval = true)
private Set<Child> children;
}
#Entity
public class Child {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne(fetch=FetchType.LAZY, optional = false)
private Parent parent;
}
When building queries with either JPA Criteria or JPQL I get a inner join by default when requesting a join fetch for children.
Either by using:
SELECT p from Parent p JOIN p.children
OR
EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Parent> query = cb.createQuery(Parent.class);
Root<Parent> root = query.from(Parent.class);
root.fetch(Parent_.children);
query.distinct(true);
List<Parent> result = em.createQuery(query).getResultList();
How I can avoid this default behavior(inner join)?
My goal is to get LEFT JOIN by default when specifying OneToMany relationship...
Is it possible without setting JoinType.LEFT explicitly when building a query?
Any help is really appreciated.
Thank you in advance
Is it possible without setting JoinType.LEFT explicitly when building a query?
No, it's not! The implicit join is the INNER JOIN which is assumed if you navigate the association in the JPQL query.
You have to use the LEFT JOIN and FETCH in case you need the child association to be fetched as well:
SELECT p
from Parent p
LEFT JOIN FETCH p.children
or with Criteria API:
EntityManager em = ...;
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Parent> query = cb.createQuery(Parent.class);
Root<Parent> root = query.from(Parent.class);
root.fetch(Parent_.children, JoinType.LEFT);
query.distinct(true);
List<Parent> result = em.createQuery(query).getResultList();
You could give Blaze-Persistence a try as it does implicit joins the way you'd expect it, i.e. only does an inner join if the row count is preserved.
List<Parent> result = cbf.create(em, Parent.class).fetch("children").getResultList();
generates
SELECT p
FROM Parent p
LEFT JOIN FETCH p.children

Categories

Resources