Persistence top

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE

1. Overview

Managing the SQL statements from our applications is one of the most important things we need to take care of because of its huge impact on performance. When working with relations between objects, there are two main design patterns for fetching. The first one is the lazy approach, while the other one is the eager approach.

In this article, we'll take an overview of both of them. In addition, we'll discuss the @LazyCollection annotation in Hibernate.

2. Lazy Fetching

We use lazy fetching when we want to postpone the data initialization until we need it. Let's look at an example to better understand the idea.

Suppose we have a company that has multiple branches in the city. Every branch has its own employees. From the database perspective, it means we have a one-to-many relation between the branch and its employees.

In the lazy fetching approach, we won't fetch the employees once we fetch the branch object. We only fetch the data of the branch object, and we postpone loading the list of employees until we call the getEmployees() method. At that point, another database query will be executed to get the employees.

The benefit of this approach is that we reduce the amount of data loaded initially. The reason is that we might not need the employees of the branch, and there's no point in loading them since we aren't planning to use them right away.

3. Eager Fetching

We use eager fetching when the data needs to be loaded instantly. Let's take the same example of the company, branches, and the employees to explain this idea as well. Once we load some branch object from the database, we'll immediately load the list of its employees as well using the same database query.

The main concern when using the eager fetching is that we load a huge amount of data that might not be needed. Hence, we should only use it when we're sure that the eagerly fetched data will always be used once we load its object.

4. The @LazyCollection Annotation

We use the @LazyCollection annotation when we need to take care of the performance in our application. Starting from Hibernate 3.0, @LazyCollection is enabled by default. The main idea of using the @LazyCollection is to control whether the fetching of data should be using the lazy approach or the eager one.

When using @LazyCollection, we have three configuration options for the LazyCollectionOption setting: TRUE, FALSE, and EXTRA. Let's discuss each of them independently.

4.1. Using LazyCollectionOption.TRUE

This option enables the lazy fetching approach for the specified field and is the default starting from Hibernate version 3.0. Therefore, we don't need to explicitly set this option. However, to explain the idea in a better way, we'll take an example where we set this option.

In this example, we have a Branch entity that consists of an id, name, and a @OneToMany relation to the Employee entity. We can notice that we set the @LazyCollection option explicitly to true in this example:

@Entity
public class Branch {

    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = "branch")
    @LazyCollection(LazyCollectionOption.TRUE)
    private List<Employee> employees;
    
    // getters and setters
}

Now, let's take a look at the Employee entity that consists of an id, name, address, as well as a @ManyToOne relation with the Branch entity:

@Entity
public class Employee {

    @Id
    private Long id;

    private String name;

    private String address;
    
    @ManyToOne
    @JoinColumn(name = "BRANCH_ID") 
    private Branch branch; 

    // getters and setters 
}

In the above example, when we get a branch object, we won't load the list of employees immediately. Instead, this operation will be postponed until we call the getEmployees() method.

4.2. Using LazyCollectionOption.FALSE

When we set this option to FALSE, we enable the eager fetching approach. In this case, we need to explicitly specify this option because we'll be overriding Hibernate's default value. Let's look at another example.

In this case, we have the Branch entity, which contains id, name, and a @OneToMany relation with the Employee entity. Note that we set the option of @LazyCollection to FALSE:

@Entity
public class Branch {

    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = "branch")
    @LazyCollection(LazyCollectionOption.FALSE)
    private List<Employee> employees;
    
    // getters and setters
}

In the above example, when we get a branch object, we'll load the branch with the list of employees instantly.

4.3. Using LazyCollectionOption.EXTRA

Sometimes, we're only concerned with the properties of the collection, and we don't need the objects inside it right away.

For example, going back to the Branch and the Employees example, we could just need the number of employees in the branch while not caring about the actual employees' entities. In this case, we consider using the EXTRA option. Let's update our example to handle this case.

Similar to the case before, the Branch entity has an id, name, and an @OneToMany relation with the Employee entity. However, we set the option for @LazyCollection to be EXTRA:

@Entity
public class Branch {

    @Id
    private Long id;

    private String name;

    @OneToMany(mappedBy = "branch")
    @LazyCollection(LazyCollectionOption.EXTRA)
    @OrderColumn(name = "order_id")
    private List<Employee> employees;

    // getters and setters
    
    public Branch addEmployee(Employee employee) {
        employees.add(employee);
        employee.setBranch(this);
        return this;
    }
}

We notice that we used the @OrderColumn annotation in this case. The reason is that the EXTRA option is taken into consideration only for indexed list collections. That's means if we didn't annotate the field with @OrderColumn, the EXTRA option will give us the same behavior as lazy and the collection will be fetched when accessed for the first time.

In addition, we define the addEmployee() method as well, because we need the Branch and the Employee to be synchronized from both sides. If we add a new Employee and set a branch for him, we need the list of employees inside the Branch entity to be updated as well.

Now, when persisting one Branch entity that has three associated employees, we'll need to write the code as:

entityManager.persist(
  new Branch().setId(1L).setName("Branch-1")

    .addEmployee(
      new Employee()
        .setId(1L)
        .setName("Employee-1")
        .setAddress("Employee-1 address"))
  
    .addEmployee(
      new Employee()
        .setId(2L)
        .setName("Employee-2")
        .setAddress("Employee-2 address"))
  
    .addEmployee(
      new Employee()
        .setId(3L)
        .setName("Employee-3")
        .setAddress("Employee-3 address"))
);

If we take a look at the executed queries, we'll notice that Hibernate will insert a new Branch for Branch-1 first. Then it will insert Employee-1, Employee-2, then Employee-3.

We can see that this is a natural behavior. However, the bad behavior in the EXTRA option is that after flushing the above queries, it'll execute three additional ones – one for every Employee we add:

UPDATE EMPLOYEES
SET
    order_id = 0
WHERE
    id = 1
     
UPDATE EMPLOYEES
SET
    order_id = 1
WHERE
    id = 2
 
UPDATE EMPLOYEES
SET
    order_id = 2
WHERE
    id = 3

The UPDATE statements are executed to set the List entry index. This is an example of what's known as the N+1 query issue, which means that we execute N additional SQL statements to update the same data we created.

As we noticed from our example, we might have the N+1 query issue when using the EXTRA option.

On the other hand, the advantage of using this option is when we need to get the size of the list of employees for every branch:

int employeesCount = branch.getEmployees().size();

When we call this statement, it'll only execute this SQL statement:

SELECT
    COUNT(ID)
FROM
    EMPLOYEES
WHERE
    BRANCH_ID = :ID

As we can see, we didn't need to store the employees' list in memory to get its size. Nevertheless, we advise avoiding the EXTRA option because it'll execute additional queries.

It's also worth noting here that it's possible to encounter the N+1 query issue with other data access technologies, as it isn't only restricted to JPA and Hibernate.

5. Conclusion

In this article, we discussed the different approaches to fetching an object's properties from the database using Hibernate.

First, we discussed lazy fetching with an example. Then, we updated the example to use eager fetching and discussed the differences.

Finally, we showed an extra approach to fetching the data and explained its advantages and disadvantages.

As always, the code presented in this article is available over on GitHub.

Persistence bottom
Get started with Spring Data JPA through the reference Learn Spring Data JPA course: >> CHECK OUT THE COURSE
Persistence footer banner
Comments are closed on this article!