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

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.

Related

Transforming an sql query into CriteriaQuery

I have two tables and they maintain the parent-child relationship between them by a foreign key.
The query looks something like below. I want to use the criteriaquery along with jpa. So can anyone help me with the criteriaquery & how the two entity classes would look like
ps:if there is any custom enity class required apart from these two entities classes help me with that as well.
Select parent.notification_id,parent.city,parent.name,parent.accountNo,
case when child.accountNo is not null then 'Yes' else 'No' end as checked
FROM parent
JOIN child ON parent.notification_id=child.notification_id_child
AND child.accountNo='test' WHERE parent.city='delhi' or parent.city='all' or parent.accountNo="test";
The column 'notification_id_child' of table 'child' is the foreign key and refers to the primarykey of table 'parent'.
There are multiple strategies that you can use to implement this:
MappedSuperclass (Parent class will be mapped with this annotation and not entity)
Single Table (Single table for each hierarchy, you can use #DiscriminatorColumn JPA annotation for identifying each hierarchy)
Joined Table (Each class for the parent and child)
In this scenario, you would have to join both the tables on the common column to fetch the results.
These are some good answers on joining tables
Joining two table entities in Spring Data JPA
Link for some good answers on usage of discrimintaorColumn
How to access discriminator column in JPA
Finally, I managed to solve the problem. My entity classes and criteria query looks something like the below.
Parent Entity
#Entity
#Table(name="parent")
public class Parent{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="notification_id")
private Long notificationId;
#Column(name="city")
private String city;
#Column(name="name")
private String name;
#Column(name="accountNo")
private String accountNo;
#JoinColumn(name="notification_id_child")
#OneToMany
private List<Child> child;
//Getters Setters
}
Child Entity
#Entity
#Table(name="child")
public class Child{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private Long id;
#Column(name="accountNo")
private String accountNo;
#Column(name="notification_id_child")
private String notificationIdChild;
//Getters Setters
}
Custom Entity
public class CustomEntity{
private Long notificationId;
private String city;
private String accountNo;
private String checked;
}
Criteria Query
#PersistenceContext
EntitiManager em;
CriteraBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<CustomEntity> cq = cb.createQuery(CustomEntity.class);
Root<Parent> parentEntity = cq.from(Parent.class);
Join<Parent,Child> join = parentEntity.join("child", JoinType.LEFT);
join.on(cb.equal(join.get("accountNo"),"test"));
Path<String> notificationIdPath = parentEntity.get("notificationId");
Path<String> cityPath = parentEntity.get("city");
Path<String> accountNoPath = parentEntity.get("accountNo");
cq.multiselect(notificationIdPath, cityPath, accountNoPath,
cb.selectCase().when(join.get("accountNo").isNotNull(),"Yes").otherwise("No"));
Path<String> accountNoPath = parentEntity("accountNo");
Predicate accountNoPredicate = cb.equal(accountNoPath, "test");
Predicate cityPredicateAll = cb.equal(cityPath,"all");
Predicate cityPredicateSpecified = cb.equal(cityPath,"delhi");
cq.where(cb.or(cityPredicateAll, cityPredicateSpecified, accountNoPredicate));
TypedQuery<CustomEntity> query = em.createQuery(cq);
List<CustomEntity> CustomEntityList = query.getResult();

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.

JPA #OneToMany Fetch Overrides CriteriaQuery Join Condition

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.

Initializing many-to-many association in Hibernate with join table

I have a Company entity that I fetch with a JPQL query with Hibernate. The entity has a many-to-many association with a Keyword entity. Since the join table has an additional column is_active, this table has been mapped to a CompanyKeyword entity. So the association is like this:
Company <-- CompanyKeyword --> Keyword
Now, the association from the Company entity is lazy, and it is not initialized by my JPQL query, as I want to avoid creating a cartesian product performance problem. That is why I want to initialize the association after running the JPQL query, e.g. like this:
#Service
class CompanyServiceImpl implements CompanyService {
#Autowired
private CompanyRepository companyRepository;
#Transactional
public Company findOne(int companyId) {
Company company = this.companyRepository.findOneWithSomeCustomQuery(companyId);
Hibernate.initialize(company.companyKeywords());
return company;
}
}
For a "normal" many-to-many association, this would work great, as all of the associated entities would be fetched in a single query. However, since I have an entity between Company and Keyword, Hibernate will only initialize the first part of the association, i.e. from Company to CompanyKeyword, and not from CompanyKeyword to Keyword. I hope that makes sense. I am looking for a way to initialize this association all the way without having to do something like this:
Company company = this.companyRepository.findOneWithSomeCustomQuery(companyId);
Hibernate.initialize(company.getCompanyKeywords());
for (CompanyKeyword ck : company.getCompanyKeywords()) {
Hibernate.initialize(ck.getKeyword());
}
The above code is neither clean, nor good in terms of performance. If possible, I would like to stick to my current approach of using a JPQL query to fetch my Company entity and then initializing certain associations afterwards; it would take quite a bit of refactoring to change this in my project. Should I just "manually" fetch the association with a second JPQL query, or is there a better way of doing it that I haven't thought of?
Below are my mappings. Thanks in advance!
Company
#Entity
#Table(name = "company")
public class Company implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
#Size(max = 20)
#OneToMany(fetch = FetchType.LAZY, mappedBy = "company")
private Set<CompanyKeyword> companyKeywords = new HashSet<>();
// Getters and setters
}
CompanyKeyword
#Entity
#Table(name = "company_service")
#IdClass(CompanyServicePK.class)
public class CompanyKeyword implements Serializable {
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Company.class)
#JoinColumn(name = "company_id")
private Company company;
#Id
#ManyToOne(fetch = FetchType.LAZY, targetEntity = Keyword.class)
#JoinColumn(name = "keyword_id")
private Keyword keyword;
#Column(nullable = true)
private boolean isActive;
// Getters and setters
}
CompanyKeywordPK
public class CompanyServicePK implements Serializable {
private Company company;
private Service service;
public CompanyServicePK() { }
public CompanyServicePK(Company company, Service service) {
this.company = company;
this.service = service;
}
// Getters and setters
// hashCode()
// equals()
}
Keyword
#Entity
#Table(name = "keyword")
public class Keyword {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column
private int id;
// Fields and getters/setters
}
You'll indeed need to execute an additional JPQL query, fetching the company with its companyKeyWords and with the keyword of each CompanyKeyWord.
You could also doing it by simply looping and initializing every entity, and still avoid executing too many queries, by enabling batch fetching.

fetch data in ManyToOne relation using Restriction

There are two tables with #OneToMany and #ManyToOne bidirectional relation, like this:
#Entity
public class Asset {
private int id;
private int count;
#OneToMany
private Set<Dealing> dealings;
...
}
#Entity
public class Dealing {
private int id;
...
#ManyToOne
#JoinColumn(name = "customer_id", nullable = false, updatable = false)
private Customer customer;
#ManyToOne
#JoinColumn(name = "product_id", nullable = false, updatable = false)
private Product product;
#ManyToOne(cascade = CascadeType.ALL)
private Asset asset;
}
all things sound OK, but when I want to search data using Restriction like this,
session.createCriteria(Asset.class).add(Restrictions.eq("dealings.customer.id", customerId)).add(Restrictions.eq("dealing.product.id", productId)).list();
In this level I get this error,
could not resolve property: dealings.customer of: com.project.foo.model.Asset
one of the solutions are to change my strategy but i wasted time to find this,btw I don't have any idea about it, do you ?
First of all, you don't have a bidirectional OneToMany association, but two unrelated unidirectional associations. In a bidirectional OneToMany association the One side must be marked as the inverse of the Many side using the mappedBy attribute:
#OneToMany(mappedBy = "asset")
private Set<Dealing> dealings;
Second, using the criteria API for such static queries is overkill, and leads to code that is harder to read than necessary.I would simply use HQL which is much easier to read. Criteria should be used for dynamic queries, IMHO, but not for static ones:
select asset from Asset asset
inner join asset.dealings dealing
where dealing.customer.id = :customerId
and dealing.product.id = :productId
Whether you use HQL or Criteria, you can't use asset.dealings.customer, since asset.dealings is a collection. A collection doesn't have a customer attribute. To be able to reference properties from the Dealing entity, you need a join, as shown in the above HQL query. And it's the same for Criteria:
Criteria criteria = session.createCriteria(Asset.class, "asset");
criteria.createAlias("asset.dealings", "dealing"); // that's an inner join
criteria.add(Restrictions.eq("dealing.customer.id", customerId);
criteria.add(Restrictions.eq("dealing.product.id", productId);

Categories

Resources