This post covers issues that may occur when you map entities with
ManyToOne bidirectional association. Read about mapping this relation while avoiding LazyInitializatinException, circular dependencies, n+1 select problem, wasting resources and false-positives in tests.
What we are going to build
In this example we are going to map the relation between meals and categories in a Spring Boot project using Hibernate. The project is build as a REST service. Therefore, we can easily call for all data we need to evaluate the correctness of our model.
You can examine the model on the picture below:
You can find the example project used in this post in the hibernate-basics-cheatsheet repository. Feel free to clone the repo and explore the hibernate features on your own.
- We are working on a
Spring Boot 2.1.1project with the
JPAdependencies. You can read about setting up this project with Spring Initializr in How to create a new Spring Boot Project.
- As usual, when working with a REST API, we are using Postman to send requests and verify API responses.
Java 11– In case of problems with building an app, the Debugging the “Fatal error compiling: invalid target release: 11” issue. How to switch your development environment and IntelliJ settings to Java 11 post may help you find the reason.
- To get the proper
LazyInitializationExceptionwarnings I added the following line to the
Don’t forget about it if your are building a project on your own! You can find more details in the Spring Boot best practice – disable OSIV to start receiving LazyInitializationException warnings again post.
- I also set the following
application propertiesto see the formatted Hibernate SQL queries in the console:
Map ManyToOne bidirectional association
Many meals can belong to one category. We want to:
- ask for meals and receive the categories associated with them in the same response;
- get a Category object and have all associated meals accessible from it.
Meal entity we are going to define the
category property with the
Thanks to the default fetch type, in this case —
Meal instance encloses the related category. In other words, when we get a meal, it will be delivered with information about its category by default.
Category entity we are going to define the
meals property with the
The default fetch type for this association is
LAZY. The related meals won’t be loaded with the requested category. We will have to explicitly ask for them. Because we run this example in a REST API, we need to use
@JsonIgnore annotation to avoid the following error:
Category entity is already detached from the session when Jackson tries to access associated meals that haven’t been loaded. We have to ignore meals in the expected JSON response. Otherwise, the API will throw a
LazyInitializationException when trying to build the response.
Identify the owning side
Relational databases operate only on unidirectional relations — using foreign keys. To properly map our bidirectional relation between entities to unidirectional relation between tables, we need to specify which table owns the
Since one category can have a vast amount of meals, we don’t want Hibernate to track changes in meals from the category side. You can check on the schema of the database, in the beginning of this post, that the
meal table holds the
category_id field as the
To reflect that in our mapping, we use
mappedBy="category" when defining the
OneToMany side in the
Category entity. That tells Hibernate to track the other side of the association — modification made on this side (Category) are already mapped by the other side (Meal::category).
Hibernate — What is happening under the hood when we ask for meals
For now, lets’ request all meals from our API in Postman:
The response contains all meals available. Every one of them comes with the instance of the
Category that is associated with it (categories don’t contain meals in the response).
We can examine all queries performed by Hibernate in the console (see
application properties in the Requirements section). Below you can see the query for all meals:
For every category associated with an existing meal there is another query. It’s the consequence of the default
EAGER fetch type for the
ManyToOne association between meals and categories.
The select for BREAKFAST category:
The select for SNACK category:
data.sql file we prepared also a third category — LUNCH, but its ignored in this case, as there is no meal associated with it.
If you don’t need categories when asking for meals, change the fetch type:
Later in this post (n+1 select issue in action), we are going to consider consequences of the default Hibernate behavior.
How to load lazy objects in Hibernate
Let’s ask for all categories in Postman:
We can see the following query in the console:
The query asks only for the id and name of every category, there is no mention of the related meals. That’s why we had to use
@JsonIgnore — to ignore meals in the JSON body.
The reason why
LAZY fetch type is applied by default for the
OneToMany relation is to optimize queries to the database. Loading nobody knows how many meals every time we ask for a category could significantly lower the performance of our API. Therefore, Hibernate by default provides us only with categories. Trying to access the related meals after categories are retrieved from the database will result in:
The solution is not to stop Hibernate from detaching retrieved objects from the persistence layer. It will allow us to reach for missing data but also introduce the n+1 select issue
To examine the n+1 select problem, we are going to add the
@Transactional on a function that calls for categories and try to access meals:
Thanks to that, the meals will be retrieved from the database when we ask for them, for every category, including the one that has no meals so far (the empty array in the middle):
That code will work. The problem gets obvious when we examine the queries printed in the console. First, the database is called for all categories like was shown in the beginning of this section. But when we iterate over the categories to print meals, the database receive one query for meals per every category, like the example one shown below:
Then the requests for the remaining categories are sent respectively. If you have to operate on many categories, you may notice performance issues.
I’m going to remove the
@Transactional annotation from the Category controller method, because I put it there only to expose the n+1 problem.
We observed the same issue when asking for meals. Default
EAGER fetch type in the relation between
Category results in performing one query for all meals and multiple queries for all associated categories.
Don’t waste resources and deal with n+1 select problem
We made the
Meal-Category relation bidirectional because we needed to be able to call both entities for related objects. To ensure that we are not wasting resources, we have to be certain about what we really need to receive from the API – complete associated objects, just their ids or maybe we won’t be really reaching to the category for its meals and the association could be unidirectional?
|Always measure the performance of the application before and after the optimization. Apply change only where it is needed.|
You need to access complete Meal instances
We need the categories to already contain associated meals when we retrieve them from the database. In order to achieve this, we are going to use the following custom query:
Which gives us the following response when the
http://localhost:8080/api/categories endpoint is hit in Postman:
In the console we will see the following query:
If you need allergens associated with meals when using
Category instance, add one more JOIN to the query:
You can use the following custom query in
MealRepository to tell
Hibernate to retrieve all meals and related categories in one query:
The above code will result in the following query:
All meals and related categories are now loaded with only one query.
The database executes only one select, but we are loading unspecified amount of full objects when requesting categories. It can be troublesome if the objects are considerably large and/or have many relations/related collections. If loading full objects decreases the API performance, consider loading only ids of the associated objects.
You only need the ids of associated meals when asking for categories
Check out the How to get json response only with an id of the related entity post to see how you can fetch an entity with an id of a related object while preserving proper serialization and deserialization. The post covers using
@JsonIdentityInfo annotation alongside with
@JsonIdentityReference and deals with common pitfalls when converting an entity with only ids of associated objects into a JSON response.
Avoid circular calls
Now, when the response for the query for categories includes the associated meals, it may be tempting to remove the
@JsonIgnore annotation from the
meals property in the
Category entity. However, the API won’t be able to properly serialize responses to JSON, due to a cyclic dependency.
The response with all categories will contain meals and every meal in it contains a category that contains meals… and so on. When the response is being converted to the JSON we get the following error:
Avoid false-positives when testing associations
The following tests show that categories loaded with
findAll() method enhanced with our custom query contain associated meals and a category loaded with the default
findById() method doesn’t include its meals:
We had to manually detach categories from the persistent layer(line 16 and line 24). Why? Every test runs within a transaction. We can ask for categories and ask for meals later, and we won’t receive the LazyInitialization exception, because Category instances never leave the transaction. They stay attached to the entity manager for the whole test.
Make sure to detach entities from the persistence layer manually in tests (and to disable OSIV) if you want the
LazyInitializationException to be thrown.
Don’t hesitate to print SQL Hibernate queries in the console when working on a data model for your API. Remember to enable it only on the development environment.
It’s crucial to see what is actually happening when we reach for entities. Spring Data and Hibernate do a great job in hiding implementation details from developers to facilitate their work. However, always dedicate enough time and attention to make sure that the data model is mapped in a proper and efficient way.