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.
Requirements
- We are working on a
Spring Boot 2.1.1
project with theWeb
andH2
andJPA
dependencies. 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.
1234$ java -versionopenjdk version "11" 2018-09-25OpenJDK Runtime Environment 18.9 (build 11+28)OpenJDK 64-Bit Server VM 18.9 (build 11+28, mixed mode)- To get the proper
LazyInitializationException
warnings I added the following line to theapplication.properties
file:
12# hibernate-basics-cheatsheet/src/main/resources/application.propertiesspring.jpa.open-in-view=falseDon’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 properties
to see the formatted Hibernate SQL queries in the console:
1234# hibernate-basics-cheatsheet/src/main/resources/application-development.propertieslogging.level.org.hibernate.SQL=debugspring.jpa.properties.hibernate.format_sql=truelogging.level.org.hibernate.type.descriptor.sql=trace
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.
For the Meal
entity we are going to define the category
property with the @ManyToOne
annotation:
1 2 3 4 5 6 7 8 |
// hibernate-basics-cheatsheet/src/main/java/in/keepgrowing/hibernatebasicscheatsheet/meal/model/Meal.java … @Entity public class Meal { … @ManyToOne private Category category; … |
Thanks to the default fetch type, in this case — EAGER
, every 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.
For the Category
entity we are going to define the meals
property with the @OneToMany
annotation:
1 2 3 4 5 6 7 8 9 |
// hibernate-basics-cheatsheet/src/main/java/in/keepgrowing/hibernatebasicscheatsheet/meal/model/Category.java … @Entity public class Category { … @OneToMany(mappedBy = "category") @JsonIgnore private List<Meal> meals; … |
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:
1 2 3 |
"Could not write JSON: failed to lazily initialize a collection of role: in.keepgrowing.hibernatebasicscheatsheet.category.Category.meals, could not initialize proxy - no Session(…)" |
The 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 FK
.
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 FK
.
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:
1 2 3 4 5 6 7 |
select meal0_.id as id1_2_, meal0_.category_id as category4_2_, meal0_.gluten_presence as gluten_p2_2_, meal0_.name as name3_2_ from meal meal0_ |
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:
1 2 3 4 5 6 7 |
select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from category category0_ where category0_.id=1 |
The select for SNACK category:
1 2 3 4 5 6 7 |
select category0_.id as id1_1_0_, category0_.name as name2_1_0_ from category category0_ where category0_.id=2 |
In the 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:
1 2 3 4 5 6 |
// hibernate-basics-cheatsheet/src/main/java/in/keepgrowing/hibernatebasicscheatsheet/meal/model/Meal.java … @ManyToOne(fetch = FetchType.LAZY) @JsonIgnore private Category category; … |
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:
1 2 3 4 5 |
select category0_.id as id1_1_, category0_.name as name2_1_ from category category0_ |
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.
n+1 select issue in action
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:
1 2 3 |
LazyInitializationException: failed to lazily initialize a collection of role: in.keepgrowing.hibernatebasicscheatsheet.category.Category.meals, could not initialize proxy - no Session |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// hibernate-basics-cheatsheet/src/main/java/in/keepgrowing/hibernatebasicscheatsheet/category/CategoryController.java … @RestController @RequestMapping("api/categories") public class CategoryController { private CategoryService service; public CategoryController(CategoryService service) { this.service = service; } @GetMapping @Transactional public List<Category> findAll() { List<Category> categories = service.findAll(); categories.forEach(c->System.out.println(c.getMeals())); return categories; } … |
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):
1 2 3 |
[Meal{id=1, name='Oatmeal', glutenPresence=GLUTEN_FREE, category=Category{id=1, name='BREAKFAST'}}] [] [Meal{id=2, name='Almond cookie', glutenPresence=CONTAINS_GLUTEN, category=Category{id=2, name='SNACK'}}, Meal{id=3, name='Chocolate cookie', glutenPresence=CONTAINS_GLUTEN, category=Category{id=2, name='SNACK'}}] |
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:
1 2 3 4 5 6 7 8 9 10 11 |
select meals0_.category_id as category4_2_0_, meals0_.id as id1_2_0_, meals0_.id as id1_2_1_, meals0_.category_id as category4_2_1_, meals0_.gluten_presence as gluten_p2_2_1_, meals0_.name as name3_2_1_ from meal meals0_ where meals0_.category_id=1 |
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 Meal
and 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:
1 2 3 4 5 6 7 8 9 |
// hibernate-basics-cheatsheet/src/main/java/in/keepgrowing/hibernatebasicscheatsheet/category/CategoryRepository.java … public interface CategoryRepository extends CrudRepository<Category, Long> { @Query("SELECT DISTINCT c" + " FROM Category c" + " LEFT JOIN FETCH c.meals") List<Category> findAll(); … |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
select distinct category0_.id as id1_1_0_, meals1_.id as id1_2_1_, category0_.name as name2_1_0_, meals1_.category_id as category4_2_1_, meals1_.gluten_presence as gluten_p2_2_1_, meals1_.name as name3_2_1_, meals1_.category_id as category4_2_0__, meals1_.id as id1_2_0__ from category category0_ left outer join meal meals1_ on category0_.id=meals1_.category_id |
If you need allergens associated with meals when using Category
instance, add one more JOIN to the query:
1 2 3 4 5 6 7 8 |
// hibernate-basics-cheatsheet/src/main/java/in/keepgrowing/hibernatebasicscheatsheet/category/CategoryRepository.java … @Query("SELECT DISTINCT c" + " FROM Category c" + " LEFT JOIN FETCH c.meals m" + " LEFT JOIN FETCH m.allergen") List<Category> findAll() … |
You can use the following custom query in MealRepository
to tell Hibernate
to retrieve all meals and related categories in one query:
1 2 3 4 5 6 7 |
// hibernate-basics-cheatsheet/src/main/java/in/keepgrowing/hibernatebasicscheatsheet/meal/model/MealRepository.java … @Query("SELECT m" + " FROM Meal m" + " LEFT JOIN FETCH m.category c") List<Meal> findAll(); … |
The above code will result in the following query:
1 2 3 4 5 6 7 8 9 10 11 12 |
select meal0_.id as id1_2_0_, category1_.id as id1_1_1_, meal0_.category_id as category4_2_0_, meal0_.gluten_presence as gluten_p2_2_0_, meal0_.name as name3_2_0_, category1_.name as name2_1_1_ from meal meal0_ left outer join category category1_ on meal0_.category_id=category1_.id |
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:
1 2 3 4 5 6 7 |
Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: Meal["category"]->Category["meals"]… |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// hibernate-basics-cheatsheet/src/test/java/in/keepgrowing/hibernatebasicscheatsheet/category/CategoryRepositoryTest.java … @RunWith(SpringRunner.class) @DataJpaTest public class CategoryRepositoryTest { @Autowired private CategoryRepository repository; @Autowired private TestEntityManager entityManager; @Test public void findsAll() { List<Category> categories = repository.findAll(); categories.forEach((c)-> entityManager.detach(c)); Meal firstMeal = categories.get(0).getMeals().get(0); assertEquals("Oatmeal", firstMeal.getName()); } @Test(expected = LazyInitializationException.class) public void findsById() { Optional<Category> category = repository.findById(1L); entityManager.detach(category.get()); List<Meal> meals = category.get().getMeals(); meals.forEach(m-> System.out.println(m.getName())); } } |
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.
Conclusion
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.
Photo by Antonio Barroro on StockSnap
Great post,
However I have one issue:
When I query all child records, only the first child record that was mapped to a particular parent will have the parent entity object in it. the rest will have an id referencing the parent enity.
Here is an example: Getting all children from POSTMAN returns:
As you can see
child2
only has"parent": 1
sincechild1
mapped to that parent first! Similarlychild4
only has"parent": 2
sincechild3
mapped to that parent first!Can anyone explain this behaviour please? I tried
fetch = FetchType.EAGER
on parent but it did not help! I expect all children to have a comprehensive parent object to prevent another DB trip.Thanks in advance!
I posed the question on stackoverflow… yet to get a response!
https://stackoverflow.com/questions/61962472/jpa-jparepository-child-record-has-parent-id-instead-of-entity-record-if-paren
Thank you for letting me know. I’ll include this issue in the post. I’m happy to see that you found the answer on Stack Overflow. Thank you once again for the comment and for including the link to your question on Stack Overflow.
Hi , in my case I have parent entity and three child entity with onetomany relationship but I want response from only first child not with second and third how to ignore that being queried…