Best Practices for Many-To-One and One-To-Many Association Mappings
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
When you model your database, you will most likely define several many-to-one or one-to-many associations. And it’s, of course, the same when you model your entities. It’s quite easy to do that with JPA and Hibernate. You just need an attribute that represents the association and annotate it with a @ManyToOne or @OneToMany association. But as easy as it seems, there are several pitfalls that you can avoid by following a few best practices.
@Entity public class Item { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fk_order") private PurchaseOrder order; ... }
@Entity public class PurchaseOrder { @OneToMany(mappedBy = "order") private List<Item> items = new ArrayList<Item>(); ... }
Don’t use unidirectional one-to-many associations
Bidirectional one-to-many and both many-to-one association mappings are fine. But you should avoid unidirectional one-to-many associations in your domain model. Otherwise, Hibernate might create unexpected tables and execute more SQL statements than you expected.
Let’s take a closer look at the standard mapping.
The definition of an unidirectional one-to-many association doesn’t seem to be an issue. You just need an attribute that maps the association and a @OneToMany relationship.
@Entity public class PurchaseOrder { @OneToMany private Set<Item> items = new HashSet<Item>(); ... }
But take a look at the SQL statements Hibernate executes when you persist a new Item entity and add it to the one-to-many association.
15:13:54,449 DEBUG SQL:92 - select nextval ('hibernate_sequence') 15:13:54,454 DEBUG SQL:92 - select items0_.PurchaseOrder_id as Purchase1_2_0_, items0_.items_id as items_id2_2_0_, item1_.id as id1_0_1_, item1_.name as name2_0_1_, item1_.version as version3_0_1_ from PurchaseOrder_Item items0_ inner join Item item1_ on items0_.items_id=item1_.id where items0_.PurchaseOrder_id=? 15:13:54,466 DEBUG SQL:92 - insert into Item (name, version, id) values (?, ?, ?) 15:13:54,468 DEBUG SQL:92 - update PurchaseOrder set version=? where id=? and version=? 15:13:54,471 DEBUG SQL:92 - insert into PurchaseOrder_Item (PurchaseOrder_id, items_id) values (?, ?)
You probably expected that Hibernate would only persist a new Item entity in the item table. I did the same when I used this mapping for the first time.
But Hibernate also retrieved all records from the PurchaseOrder_Item table that are associated with the Order entity, wrote a new record to the same table and updated a record in the PurchaseOrder table.
Why does Hibernate execute so many queries and introduce an additional association table?
In your table model, you normally use a foreign key column on the to-many side of the association to store a reference to the associated record. Hibernate uses the same approach when you model a bidirectional one-to-many or an unidirectional many-to-one relationship. It uses the foreign key column to map the association.
But it can’t do that if you don’t model the relationship on the entity, which represents the to-many side of the relationship. So, Hibernate introduces an association table to store the foreign keys.
You can avoid this table if you specify the foreign key column with a @JoinColumn annotation. This column has to be part of the table of the to-many side of the association. So, in this example, the item table has to have a fk_order column which stores a foreign key to the purchaseorder table.
@Entity public class PurchaseOrder { @OneToMany @JoinColumn(name = "fk_order") private Set<Item> items = new HashSet<Item>(); ... }
As you can see in the log output, Hibernate now uses the foreign key column instead of an association table to map the relationship. But it still has to perform an additional SQL UPDATE statement to set the foreign key because the Item entity doesn’t map the foreign key column.
15:31:15,753 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=? 15:31:15,771 DEBUG SQL:92 - select nextval ('hibernate_sequence') 15:31:15,777 DEBUG SQL:92 - select items0_.fk_order as fk_order4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.name as name2_0_1_, items0_.version as version3_0_1_ from Item items0_ where items0_.fk_order=? 15:31:15,788 DEBUG SQL:92 - insert into Item (name, version, id) values (?, ?, ?) 15:31:15,790 DEBUG SQL:92 - update PurchaseOrder set version=? where id=? and version=? 15:31:15,793 DEBUG SQL:92 - update Item set fk_order=? where id=?
So, better use a bi-directional instead of a unidirectional one-to-many association.
Avoid the mapping of huge to-many associations
I know, mapped to-many associations are useful, especially when you want to join entities in a JPQL query. But Hibernate loads all associated entities when it initializes the association. That can take several seconds or even minutes when Hibernate has to fetch several thousand entities.
So, better use an unidirectional many-to-one association. You can’t use the to-many mapping anyways, and it removes the risk that someone triggers the initialization by accident.
When you need to read the associated entities, it’s better to use a JPQL query with pagination. That allows you to fetch a number of entities that you can handle in your business logic or present to the user. And after you’ve processed the retrieved entities, you can execute another query to retrieve the next set of entities until you’ve reached the end of the list.
TypedQuery<Item> q = em.createQuery("SELECT i FROM Item i JOIN FETCH i.order", Item.class); q.setFirstResult(0); q.setMaxResults(5); List<Item> items = q.getResultList();
If you need to join the associated entities in a JPQL query, you can either use the mapped many-to-one association or a Hibernate-specific JOIN clause that doesn’t require a mapped relationship.
TypedQuery<PurchaseOrder> q = em.createQuery("SELECT o FROM PurchaseOrder o JOIN Item i ON o.id = i.order.id WHERE i.id = :itemId", PurchaseOrder.class); q.setParameter("itemId", item2.getId()); q.getSingleResult();
Think twice before using CascadeType.Remove
Cascade remove is another feature that works well on small to-many associations. Using it for one-to-many or many-to-one associations is not as dangerous as it is for many-to-many relationships. But it’s very inefficient when it needs to remove a huge number of entities.
Let’s take a look at an example. The following mapping tells Hibernate to remove all associated Item entities when it deletes the PurchaseOrder entity.
@Entity public class PurchaseOrder { @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE, orphanRemoval = true) private List<Item> items = new ArrayList<Item>(); ... }
The problem with this mapping is that Hibernate needs to execute proper lifecycle transitions for all entities. So, Hibernate needs to select all associated Item entities and remove them one by one.
16:08:25,677 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=? 16:08:25,711 DEBUG SQL:92 - select items0_.fk_order as fk_order4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.name as name2_0_1_, items0_.fk_order as fk_order4_0_1_, items0_.version as version3_0_1_ from Item items0_ where items0_.fk_order=? 16:08:25,874 DEBUG SQL:92 - delete from Item where id=? and version=? 16:08:25,881 DEBUG SQL:92 - delete from Item where id=? and version=? 16:08:25,883 DEBUG SQL:92 - delete from PurchaseOrder where id=? and version=?
Deleting the associated entities one by one can create an overhead that is huge enough that you should better remove them with a JPQL query. But please be aware that Hibernate will not call any EntityListeners for these entities, and it also doesn’t remove them from any caches.
If you want to spend some extra effort, you can update the caches programmatically. The following code snippet shows an example that removes all entities from the first level cache before it calls a JPQL query to remove all Item entities associated to a given Order entity.
em.flush(); em.clear(); Query q = em.createQuery("DELETE Item i WHERE i.order.id = :orderId"); q.setParameter("orderId", orderId); q.executeUpdate(); order = em.find(PurchaseOrder.class, orderId); em.remove(order);
You first need to call the flush() method on the EntityManager to make sure that Hibernate wrote all changes to the database. Then you can call the clear() method to detach all entities from the current persistence context and to remove them from the first level cache.
After that is done, you can use a simple JPQL query to remove all associated Item entities before you read and remove the PurchaseOrder entity.
The complexity of this approach is a lot higher than using a simple cascade delete. But as you can see in the following log output, it only needs 3 queries to remove a PurchaseOrder with all associated Item entities.
16:19:18,985 DEBUG SQL:92 - delete from Item where fk_order=? 16:19:19,003 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=? 16:19:19,026 DEBUG SQL:92 - delete from PurchaseOrder where id=? and version=?
Use orphanRemoval when modeling parent-child associations
The orphanRemoval feature can make it very comfortable to remove a child entity. You can use it for parent-child relationships in which a child entity can’t exist without its parent entity.
That’s the case in the example that I use in this post. An Item entity can’t exist without a PurchaseOrder entity. So, any Item entity that’s not associated to a PurchaseOrder entity, needs to be removed.
Hibernate does that automatically when you set the orphanRemoval attribute of the @OneToMany annotation to true and the cascade attribute to CascadeType.ALL.
@Entity public class PurchaseOrder { @OneToMany(mappedBy = "order", orphanRemoval = true) private List<Item> items = new ArrayList<Item>(); ... }
You now just need to remove an Item entity from the List<Item> items attribute of the PurchaseOrder entity to delete it from the database.
order = em.find(PurchaseOrder.class, orderId); order.getItems().remove(1);
16:42:16,251 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=? 16:42:16,273 DEBUG SQL:92 - select items0_.fk_order as fk_order4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_, items0_.name as name2_0_1_, items0_.fk_order as fk_order4_0_1_, items0_.version as version3_0_1_ from Item items0_ where items0_.fk_order=? 16:42:16,295 DEBUG SQL:92 - delete from Item where id=? and version=?
Implement helper methods to update bi-directional associations
Bidirectional associations are comfortable to use in queries and to navigate relationships in your domain model. But they require special attention when you update them.
When you add an entity to or remove it from an association, you need to perform the operation on both ends. That means, that when you add a new Item to a PurchaseOrder, you need to set the PurchaseOrder on the Item and add the Item to the List<Item> on the PurchaseOrder.
Item item3 = new Item(); item3.setName("Third Item"); item3.setOrder(order); em.persist(item3); order = em.find(PurchaseOrder.class, orderId); order.getItems().add(item3);
That is an error-prone task. You should, therefore, provide helper methods that implement this logic.
@Entity public class PurchaseOrder { ... public void addItem(Item item) { this.items.add(item); item.setOrder(this); } }
Item item3 = new Item(); item3.setName("Third Item"); order.addItem(item3); em.persist(item3);
Define FetchType.LAZY for @ManyToOne association
The JPA specification defines FetchType.EAGER as the default for to-one relationships. It tells Hibernate to initialize the association, when it loads the entity. That is not a big deal, if you just load one entity. It requires just 1 additional query if you use JPQL query and Hibernate creates an INNER JOIN when you use the EntityManager.find method.
But that dramatically changes when you select multiple Item entities.
List<Item> items = em.createQuery("SELECT i FROM Item i", Item.class).getResultList();
Hibernate then needs to perform an additional query for each of the selected entities. That is often called a n+1 select issue. You can learn more about it in my free course How to find and fix n+1 select issues.
17:06:44,753 DEBUG SQL:92 - select item0_.id as id1_0_, item0_.name as name2_0_, item0_.fk_order as fk_order4_0_, item0_.version as version3_0_ from Item item0_ 17:06:44,775 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=? 17:06:44,793 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=? 17:06:44,796 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=? 17:06:44,798 DEBUG SQL:92 - select purchaseor0_.id as id1_1_0_, purchaseor0_.version as version2_1_0_ from PurchaseOrder purchaseor0_ where purchaseor0_.id=?
You can avoid that by setting the FetchType on the @ManyToOne annotation to LAZY.
@Entity public class Item { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fk_order") private PurchaseOrder order; ... }
And if you need the to-one association in your use case, you can use a JOIN FETCH clause or one of the other options to initialize lazy relationships.
List<Item> items = em.createQuery("SELECT i FROM Item i JOIN FETCH i.order", Item.class).getResultList();
Summary
One of the benefits of using JPA and Hibernate is that they make it very easy to manage associations and to use them in queries. But as you’ve seen in this post, there are a few pitfalls you should avoid.
So, when you model your next many-to-one or one-to-many association, please make sure to:
- Not use unidirectional one-to-many associations
- Avoid the mapping of huge to-many associations
- Think twice before using CascadeType.Remove
- Use orphanRemoval when modeling parent-child associations
- Implement helper methods to update bidirectional associations
- Define FetchType.LAZY for @ManyToOne association
obrigado por compartilhar essas dicas muito importantes.
Hi,
Can you please explain below line in detail ?
“Hibernate loads all associated entities when it initializes the association”.
is it during hibernate proxy or when fetching actual child ?
Thank you
Hi,
that depends on the FetchType you defined for the association.
If you use FetchType.EAGER, Hibernate fetches the association when it instantiates the entity object. This is not recommended because you will initialize the association even if you don’t use it in your code.
If you use FetchType.LAZY, Hibernate fetches the association when you use it. For all to-many associations, Hibernate does that using its own Set or List implementation. For all to-one associations, Hibernate instantiates a proxy object that triggers the required queries. This is much better because it avoids any unnecessary database queries. But it’s still an issue for huge associations because Hibernate fetches all associated entities at once.
Regards,
Thorben
Hello, I noticed you only used LAZY on the @ManyToOne relations. Isn't it possible to use LAZY on @OneToMany as well?
Thanks
Hi,
All to-many associations use FetchType.LAZY by default, so you don’t need to set it in your mapping annotations. But if you want, you can, of course, set fetch=FetchType.LAZY on the @OneToMany annotations.
Regards,
Thorben
Thank you. It’s very helpfull and detailed tutorial. Playing with Hibernate is always nightmare for me 😉
Thanks, Mat.
And working with Hibernate should no longer be a nightmare after you’ve read a few more of my articles 😉
Thank you Thorben – very helpful as always!
What do you think about this opinion (https://stackoverflow.com/a/30474303) of Spring Data author
Oliver Gierke about not using bi-directional associations?
Hi Serj,
in general, following Oliver’s suggestions is a good idea 😉
Using an unidirectional many-to-one association is definitely a valid approach. But please, don’t use a one-to-many association.
And keep in mind that JOIN clauses in JPQL require a mapped association. So, you will need to define all joins in the to-one direction. It, of course, depends on your queries if this is a problem or not.
And as you can see in his comment, he recommends similar helper methods for bi-directional associations as I did in this post.
So, to make it short:
Either use a uni-directional many-to-one association or helper methods to keep both ends of the association in sync.
Regards,
Thorben