|

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

9 Comments

  1. Avatar photo TARCISIO RITTER SCOTTA says:

    obrigado por compartilhar essas dicas muito importantes.

  2. 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

    1. Avatar photo Thorben Janssen says:

      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

      1. Avatar photo Guilherme Taffarel Bergamin says:

        Hello, I noticed you only used LAZY on the @ManyToOne relations. Isn't it possible to use LAZY on @OneToMany as well?

        Thanks

        1. Avatar photo Thorben Janssen says:

          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

  3. Thank you. It’s very helpfull and detailed tutorial. Playing with Hibernate is always nightmare for me 😉

    1. Avatar photo Thorben Janssen says:

      Thanks, Mat.
      And working with Hibernate should no longer be a nightmare after you’ve read a few more of my articles 😉

  4. Avatar photo Serj Poznanski says:

    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?

    1. Avatar photo Thorben Janssen says:

      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

Comments are closed.