Hibernate/JPA Performance. Why eager loading hurts?

Introduction

With more and more applications using Hibernate/JPA, the need for understanding the core concepts plays a very important role so as to avoid performance issues later on.

In this blog post, i will share my views on lazy & eager loading/fetching issues. I am still left with proper formatting, so please bear with me for some time if in case you see the content is not properly aligned.

In Hibernate, except for one-to-one, all other forms of associations are lazily loaded. One for one-to-one association, eager loading is used by default in Hibernate. But when using JPA, even for many-to-one association, eager loading is used by default.

Let’s come back to lazy loading for the time being 🙂

With the help of an example, we will discuss the problem in detail. I will use JPA annotations for the sake of simplicity and reduced configuration.

@Entity
@Table(name="TBL_CUSTOMER")
public class Customer {

	@Id
	@GeneratedValue
	private int id;

	private String name;
	private String email;

	@OneToMany(mappedBy="customer")
	private Set<Order> orders;

        //getters and setters	

	@Override
	public String toString() {
		return "Customer [id=" + id + ", name=" + name + ", email=" + email
				+ "]";
	}
}
@Entity
@Table(name="TBL_ORDER")
public class Order {

	@Id
	@GeneratedValue
	private int id;

	private Date orderDate;
	private double amount;

	@ManyToOne
	private Customer customer;

        //getters and setters

	@Override
	public String toString() {
		return "Order [id=" + id + ", orderDate=" + orderDate + ", amount="
				+ amount + "]";
	}
}

Now, i am going to add lot’s of customers in the database:

for(int i=1;i<=100;i++) {
	Customer customer = new Customer();
	customer.setName("A"+1);
	customer.setEmail("a"+i+"@gmail.com");
	entityManager.persist(customer);
}

And i am going to place orders for some of these customers:

for(int i=1;i<=100;i++) {
	int anyRandomCustomer = new Random().nextInt(100) + 1; 
        //assumption is that pk assigned when adding customers was in range 1-100
	Customer customer = entityManager.find(Customer.class, anyRandomCustomer);
	Order order = new Order();
	order.setAmount(i*10);
	order.setOrderDate(new Date());
	order.setCustomer(customer);
	entityManager.persist(order);
}

So we have sufficient data in the database to proceed further with.

n+1 problem. What’s the problem?

Hibernate uses lazy loading for all forms of association except one-to-one by default. Which means for one-to-many, many-to-one, many-to-many and others, hibernate will not the parent/child object graph by default.

In context of the example it means, if we load some Customer from the database, Hibernate will not load the orders placed by that customer till not forced to do so.

To understand the problem why many a times developers enable eager loading, let’s run this piece of code to see what happens:

Customer customer = entityManager.find(Customer.class, 1);
entityManager.detach(customer);

System.out.println(customer);
System.out.println(customer.getOrders());

I am using detach method just so that i can demonstrate the lazy loading behaviour. Generally an object becomes a detached object when we close the underlying Session/EntityManager associated with it, so in projects you will not always call this method. Also i’ve overriden toString() method in both the POJOs for getting some readable output. If you run the above code, this is what happens:

Hibernate: /* load Customer */ select customer0_.id as id0_0_, customer0_.email as email0_0_, customer0_.name as name0_0_ from TBL_CUSTOMER customer0_ where customer0_.id=?
Customer [id=1, name=A1, email=a1@gmail.com]
org.hibernate.LazyInitializationException: could not initialize proxy - no Session

So as you can see clearly, Customer got loaded, but Hibernate failed to load the orders placed by that customer, because to do some it requires the object to be in the attached/managed state. For detached objects, there is nothing Hibernate can do by default.

This is where we find developers switching to eager loading. Just because what we want is that when the Customer get’s loaded, the Order placed by that customer also gets loaded. So let’s modify the association to enable eager loading:

@OneToMany(fetch=FetchType.EAGER, mappedBy="customer")
private Set<Order> orders;

Let’s run the previous code again and see what happens now:

Hibernate: /* load Customer */ select customer0_.id as id0_1_, customer0_.email as email0_1_, customer0_.name as name0_1_, orders1_.customer_id as customer4_0_3_, orders1_.id as id1_3_, orders1_.id as id1_0_, orders1_.amount as amount1_0_, orders1_.customer_id as customer4_1_0_, orders1_.orderDate as orderDate1_0_ from TBL_CUSTOMER customer0_ left outer join TBL_ORDER orders1_ on customer0_.id=orders1_.customer_id where customer0_.id=?
Customer [id=63, name=A1, email=a63@gmail.com]
[Order [id=73, orderDate=2013-04-19 19:40:19.273, amount=730.0], Order [id=93, orderDate=2013-04-19 19:40:19.314, amount=930.0], Order [id=78, orderDate=2013-04-19 19:40:19.281, amount=780.0], Order [id=47, orderDate=2013-04-19 19:40:19.201, amount=470.0], Order [id=87, orderDate=2013-04-19 19:40:19.302, amount=870.0]]

Pretty good so far. But the real problem is not visible still. Let’s use the Query or Criteria API to load more than one customer to see what happens. The code we need to run now is:

Query query = entityManager.createQuery("select c from Customer c");
List<Customer> customers = query.getResultList();
for (Customer customer : customers) { 
     System.out.println(customer); 
}

Now in this situation where we are loading multiple parent rows from the database and eager loading enabled for it’s children, let’s see what happens behind the scenes by referring to the hibernate logs:

Hibernate: /* from Customer */ select customer0_.id as id0_, customer0_.email as email0_, customer0_.name as name0_ from TBL_CUSTOMER customer0_
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
Hibernate: /* load one-to-many jpa.onetomanybi.Customer.orders */ select orders0_.customer_id as customer4_0_1_, orders0_.id as id1_1_, orders0_.id as id1_0_, orders0_.amount as amount1_0_, orders0_.customer_id as customer4_1_0_, orders0_.orderDate as orderDate1_0_ from TBL_ORDER orders0_ where orders0_.customer_id=?
...

Actually that’s not the complete log, for those 100 customers that we loaded, Hibernate has fired 100 subsequent select queries to load the orders for each customer one by one. Each customer can have a history of many orders. This is what is referred to as the n+1 problem. Now this happened because we had enabled eager loading. We asked Hibernate that whenever i load a customer, you please go and get me the orders placed by that customer also loaded. Not to blame, it’s not Hibernate’s fault. I have seen people using eager loading in so many projects without thinking about the consequence of it. In my trainings i see to it that my participants are aware of this and they don’t do the same mistake in their projects.

Just imagine what will happen in a live environment where the amount of data will be more than this dummy example and a small mistake of the developer using eager loading, will cause such a huge performance bottle neck.

The question is, do we really need the orders list everytime a customer is loaded? Not really according to me, if we take the power of Query/Criteria API, we don’t have to rely on eager loading at all. With the help of joins, one can do whatever we want. So if a use case arises where we need the Customer and the Order information both, a simple query will solve the problem.

So i suggest that eager loading should only be used if we are 100% sure that the parent/child graph has to be loaded everytime in the application across all the use cases. But i believe this will be a very rare case. Now how will the Query/Criteria code look like to eagerly load the data:

Query query = entityManager.createQuery("select c from Customer c join fetch c.orders");
List<Customer> customers = query.getResultList();

for (Customer customer : customers) {
	System.out.println(customer);
}

I am using fetch keyword, which will solve the above n+1 problem. As of now there is no where condition in the query, but generally we will have some. So what i suggest is that, use lazy loading by default, and whenever there is a use case in which you need both the parent and child, a simple query will get the job done.

 

Leave a Reply

Your email address will not be published. Required fields are marked *