Are you a Hibernate user? Have you ever got a LazyInitializationException? Let’s clarify this topic a bit. You can find plenty of solutions for the exception, however they lack explanation and might suggest a risk-prone way of solving the problem.
Let’s see what are the possibilities when you want to describe an entity relation. We want to represent a Company with have multiple Products which they are selling. One can come up with the following classes:
@Entity public class Company { @Id private UUID id; @OneToMany(mappedBy = "company", cascade = CascadeType.PERSIST) private Set<Product> products = new HashSet<>(); public Company() { this.id = UUID.randomUUID(); } // getters setters omitted }
@Entity public class Product { @Id private UUID id; @Column(name = "name") private String name; @ManyToOne private Company company; public Product() { this.id = UUID.randomUUID(); } // getters setters omitted }
Simple classes with a simple relation. Talking about relations. Hibernate offers two way of relations, eager loaded relations and lazy loaded relations. The behavior can be defined through the annotation for the relation, i.e. @OneToMany, @ManyToOne, @OneToOne, @ManyToMany.
The main difference between the two is the time of loading the relation. Eager loading means that if you load a Company from the database, Hibernate will automatically load the products relation for it before you are able to use it. Lazy loading means that Hibernate will defer the loading until the relation is accessed. This is implemented through Proxy classes, for example you can take a look on PersistentSet, PersistentBag, etc. classes.
Usually it’s a good idea to make every relation lazy because each use case in an application needs different set of data. If I’m showing all the companies in a table, I only need the Company entities and in a detailed Company view, I might need the products sold by this Company. In the first case, it’s clear that there is no point loading the Products for a certain Company as it wouldn’t be used anyhow, plus it slows down the application.
Yes, loading relations can be done in various ways, but all of them implies some sort of performance degradation. Also, there are multiple ways to load lazy relations as well. You can use simple loading-on-access, fetching through Criteria API, fetching through JPQL, etc.
Let’s see the first, loading the lazy relation on access. As I mentioned earlier, lazy loading is implemented through Proxy objects. When you try to call any method on the Proxy object, it will try to fetch the relation from the database. In case of the Company/Product example, if you fetch a Company and call the getProducts method, it will return a Proxy object (some kind of Collection proxy). If you call any method on this object, Hibernate will try to load this collection from the database to make it usable. Accessing the database of course requires a Transaction. If no transaction present when accessing the relation, Hibernate will throw the well-known LazyInitializationException with the following message:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.arnoldgalovics.blog.repository.Company.products, could not initialize proxy - no Session
Check out the test case for it:
@Test(expected = LazyInitializationException.class) public void shouldFailWithLazyInitException() { Company company = underTest.getCompany(companyId); company.getProducts().size(); }
First, we retrieve the Company based on the companyId and after that we access the relation through the getProducts method. This method returns the Proxy object on which we call the size method which triggers the loading. The loading is done by executing the following SQL statement:
select products0_.company_id as company_3_1_0_, products0_.id as id1_1_0_, products0_.id as id1_1_1_, products0_.company_id as company_3_1_1_, products0_.name as name2_1_1_ from Product products0_ where products0_.company_id=?
The company_id parameter is the id of the Company which we are loading the relation for. This is done by Hibernate automatically.
The solution for this exception is either loading the relation in a Transaction or allowing Hibernate to create an ad-hoc transaction only for loading the relation.
The test case for loading the relation within a Transaction is the following:
@Test public void shouldLoadWithinTransaction() { helper.doInTransaction(em -> { Company company = underTest.getCompany(companyId); assertFalse(isInitialized(company.getProducts())); int productCount = company.getProducts().size(); assertEquals(2, productCount); }); }
The helper class is basically just a wrapper to utilize Spring’s declarative transaction management.
The other way – which is allowing Hibernate to create temporary Transactions – is not recommended as it might cause performance issues quickly. You can read more about it in this blog post.
Of course, this is not the best way of loading lazy relations because if you think about loading multiple Companies, let’s say all of them, Hibernate will execute the following query:
select company0_.id as id1_0_ from Company company0_
If you try to print out the number of Products sold by one Company in the following way:
for (Company c : companies) { System.out.println(c.getProducts().size()); }
You will end up executing this query for each Company:
select products0_.company_id as company_3_1_0_, products0_.id as id1_1_0_, products0_.id as id1_1_1_, products0_.company_id as company_3_1_1_, products0_.name as name2_1_1_ from Product products0_ where products0_.company_id=?
This means, you have to execute 1 query to retrieve all the Companies and N – numer of companies – queries to retrieve all the Products for a single Company. This is often referred to as N+1 problem.
With this implementation you will have several queries to execute which is obviously not the most efficient way of doing this. A better idea is to load the relation at the time you are loading the parent – Company – entity. This can be done with a Fetch Join.
In Criteria API, you can do the following:
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery<Company> query = cb.createQuery(Company.class); Root<Company> root = query.from(Company.class); query.select(root); root.fetch("products"); query.where(cb.equal(root.get("id"), id)); return entityManager.createQuery(query).getSingleResult();
The important part is the root.fetch(“products”) line. It will instruct Hibernate to load the products relation with the initial query. Now the query is the following:
select company0_.id as id1_0_0_, products1_.id as id1_1_1_, products1_.company_id as company_3_1_1_, products1_.name as name2_1_1_, products1_.company_id as company_3_1_0__, products1_.id as id1_1_0__ from Company company0_ inner join Product products1_ on company0_.id=products1_.company_id where company0_.id=?
As you can see, it’s joined to the root entity and the select part contains all the necessary attributes of the Product entity. This can be done through JPQL as well, there is a special JOIN FETCH statement to do it. In this way you will have only 1 query instead of N+1 which is a lot more better.
The source can be found on GitHub. Feel free to reach me out in case of questions in the comments or on Twitter.
Hi, thanks for the explanation. But how can this problem be solved whithout any query?
In my case (a car pool), I have these two models :
public abstract class Car {
@GeneratedValue @Id
private Long id;
private String name;
@ManyToMany (cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
@JoinTable (name = “Car_Person”,
joinColumns=@JoinColumn(name = “CarId”, referencedColumnName = “id”),
inverseJoinColumns=@JoinColumn(name = “personId”, referencedColumnName = “id”))
private List carUsers;
//rest omitted
}
And:
@Entity
public class Person {
@GeneratedValue @Id
private Long id;
private String name;
private int age;
@ManyToMany(mappedBy = “motUsers”, fetch = FetchType.EAGER)
private List cars = new ArrayList();
//rest omitted
}
I even get LazyInitializationException with FetchType=Fetch.EAGER, when I reference a Car in my JSF page like this:
So I thought, there is no need for a query here, since JPA is resolving the m:n relation on it´s own, right?
Any help would be highly appreciated.
vf