1. Overview
While using lazy loading in Hibernate, we might face exceptions, saying there is no session.
In this tutorial, we'll discuss how to solve these lazy loading issues. To do this, we'll use Spring Boot to explore an example.
2. Lazy Loading Issues
The aim of lazy loading is to save resources by not loading related objects into memory when we load the main object. Instead, we postpone the initialization of lazy entities until the moment they're needed. Hibernate uses proxies and collection wrappers to implement lazy loading.
When retrieving lazily-loaded data, there are two steps in the process. First, there's populating the main object, and second, retrieving the data within its proxies. Loading data always requires an open Session in Hibernate.
The problem arises when the second step happens after the transaction has closed, which leads to a LazyInitializationException.
The recommended approach is to design our application to ensure that data retrieval happens in a single transaction. But, this can sometimes be difficult when using a lazy entity in another part of the code that is unable to determine what has or hasn't been loaded.
Hibernate has a workaround, an enable_lazy_load_no_trans property. Turning this on means that each fetch of a lazy entity will open a temporary session and run inside a separate transaction.
3. Lazy Loading Example
Let's look at the behavior of lazy loading under a few scenarios.
3.1 Set Up Entities and Services
Suppose we have two entities, User and Document. One User may have many Documents, and we'll use @OneToMany to describe that relationship. Also, we'll use @Fetch(FetchMode.SUBSELECT) for efficiency.
We should note that, by default, @OneToMany has a lazy fetch type.
Let's now define our User entity:
@Entity public class User { // other fields are omitted for brevity @OneToMany(mappedBy = "userId") @Fetch(FetchMode.SUBSELECT) private List<Document> docs = new ArrayList<>(); }
Next, we need a service layer with two methods to illustrate the different options. One of them is annotated as @Transactional. Here, both methods perform the same logic by counting all documents from all users:
@Service public class ServiceLayer { @Autowired private UserRepository userRepository; @Transactional(readOnly = true) public long countAllDocsTransactional() { return countAllDocs(); } public long countAllDocsNonTransactional() { return countAllDocs(); } private long countAllDocs() { return userRepository.findAll() .stream() .map(User::getDocs) .mapToLong(Collection::size) .sum(); } }
Now, let's take a closer look at the following three examples. We'll also use SQLStatementCountValidator to understand the efficiency of the solution, by counting the number of queries executed.
3.2. Lazy Loading With a Surrounding Transaction
First of all, let's use lazy loading in the recommended way. So, we'll call our @Transactional method in the service layer:
@Test public void whenCallTransactionalMethodWithPropertyOff_thenTestPass() { SQLStatementCountValidator.reset(); long docsCount = serviceLayer.countAllDocsTransactional(); assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); SQLStatementCountValidator.assertSelectCount(2); }
As we can see, this works and results in two roundtrips to the database. The first roundtrip selects users, and the second selects their documents.
3.3. Lazy Loading Outside of a Transaction
Now, let's call a non-transactional method to simulate the error we get without a surrounding transaction:
@Test(expected = LazyInitializationException.class) public void whenCallNonTransactionalMethodWithPropertyOff_thenThrowException() { serviceLayer.countAllDocsNonTransactional(); }
As predicted, this results in an error as the getDocs function of User is used outside of a transaction.
3.4. Lazy Loading With Automatic Transaction
To fix that, we can enable the property:
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
With the property turned on, we no longer get a LazyInitializationException.
However, the count of the queries shows that six roundtrips have been made to the database. Here, one roundtrip selects users, and five roundtrips select documents for each of five users:
@Test public void whenCallNonTransactionalMethodWithPropertyOn_thenGetNplusOne() { SQLStatementCountValidator.reset(); long docsCount = serviceLayer.countAllDocsNonTransactional(); assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount); SQLStatementCountValidator.assertSelectCount(EXPECTED_USERS_COUNT + 1); }
We've run into the notorious N + 1 issue, despite the fact that we set a fetch strategy to avoid it!
4. Comparing the Approaches
Let's briefly discuss the pros and cons.
With the property turned on, we don't have to worry about transactions and their boundaries. Hibernate manages that for us.
However, the solution works slowly, because Hibernate starts a transaction for us on each fetch.
It works perfectly for demos and when we don't care about performance issues. This may be ok if used to fetch a collection that contains only one element, or a single related object in a one to one relationship.
Without the property, we have fine-grained control of the transactions, and we no longer face performance issues.
Overall, this is not a production-ready feature, and the Hibernate documentation warns us:
Although enabling this configuration can make LazyInitializationException go away, it's better to use a fetch plan that guarantees that all properties are properly initialized before the Session is closed.
5. Conclusion
In this tutorial, we explored dealing with lazy loading.
We tried a Hibernate property to help overcome the LazyInitializationException. We also saw how it reduces efficiency and may only be a viable solution for a limited number of use cases.
As always, all code examples are available over on GitHub.