ManyToOne bidirectional association in Hibernate — common issues solved

featured_image

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:

hibernate-basics-cheatsheet

 

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 the Web and H2 and JPA 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.
  • To get the proper LazyInitializationException warnings I added the following line to the application.properties file:
    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 properties to 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.

For the Meal entity we are going to define the category property with the @ManyToOne annotation:

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:

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:

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:

Postman-findAllMeals-ManyToOne-Bidirectional

 

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:

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:

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:

Postman-findAllCategories-OneToMany-Bidirectional

 

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.

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:

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 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:

Which gives us the following response when the http://localhost:8080/api/categories endpoint is hit in Postman:

Postman-findAllCategoires-hql

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.

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

Leave a Reply

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