The best way to map a @OneToMany relationship with JPA and Hibernate

Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

Introduction

In a relational database system, a one-to-many association links two tables based on a Foreign Key column so that the child table record references the Primary Key of the parent table row.

The one-to-many table relationship

As straightforward as it might be in a RDBMS, when it comes to JPA, the one-to-many database association can be represented either through a @ManyToOne or a @OneToMany association since the OOP association can be either unidirectional or bidirectional.

The @ManyToOne annotation allows you to map the Foreign Key column in the child entity mapping so that the child has an entity object reference to its parent entity. This is the most natural way of mapping a database one-to-many database association, and, usually, the most efficient alternative too.

For convenience, to take advantage of the entity state transitions and the dirty checking mechanism, many developers choose to map the child entities as a collection in the parent object, and, for this purpose, JPA offers the @OneToMany annotation.

As I explained in my book, many times, you are better off replacing collections with a query, which is much more flexible in terms of fetching performance. However, there are times when mapping a collection is the right thing to do, and then you have two choices:

  • a unidirectional @OneToMany association
  • a bidirectional @OneToMany association

The bidirectional association requires the child entity mapping to provide a @ManyToOne annotation, which is responsible for controlling the association.

One the other hand, the unidirectional @OneToMany association is simpler since it’s just the parent-side which defines the relationship. In this article, I’m going to explain the caveats of @OneToMany associations, and how you can overcome them.

There are many ways to map the @OneToMany association. We can use a List or a Set. We can also define the @JoinColumn annotation too. So, let’s see how all these work.

Unidirectional @OneToMany

Consider we have the following mapping:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(
        cascade = CascadeType.ALL, 
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();

    //Constructors, getters and setters removed for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    @GeneratedValue
    private Long id;

    private String review;

    //Constructors, getters and setters removed for brevity
}

Now, if we persist one Post and three PostComment(s):

Post post = new Post("First post");

post.getComments().add(
    new PostComment("My first review")
);
post.getComments().add(
    new PostComment("My second review")
);
post.getComments().add(
    new PostComment("My third review")
);

entityManager.persist(post);

Hibernate is going to execute the following SQL statements:

insert into post (title, id) 
values ('First post', 1)

insert into post_comment (review, id) 
values ('My first review', 2) 

insert into post_comment (review, id) 
values ('My second review', 3)

insert into post_comment (review, id) 
values ('My third review', 4)

insert into post_post_comment (Post_id, comments_id) 
values (1, 2)

insert into post_post_comment (Post_id, comments_id) 
values (1, 3)

insert into post_post_comment (Post_id, comments_id) 
values (1, 4)

What is that! Why there are so many queries executed? And what’s the deal with that post_post_comment table anyway?

Well, by default, that’s how the unidirectional @OneToMany association works, and this is how it looks from a database perspective:

Unidirectional one-to-many JPA relationship

For a DBA, this looks more like a many-to-many database association than a one-to-many relationship, and it’s not very efficient either. Instead of two tables, we now have three tables, so we are using more storage than necessary. Instead of only one Foreign Key, we now have two of them. However, since we are most likely going to index these Foreign Keys, we are going to require twice as much memory to cache the index for this association. Not nice!

Unidirectional @OneToMany with @JoinColumn

To fix the aforementioned extra join table issue, we just need to add the @JoinColumn in the mix:

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "post_id")
private List<PostComment> comments = new ArrayList<>();

The @JoinColumn annotation helps Hibernate (the most famous JPA provider) to figure out that there is a post_id Foreign Key column in the post_comment table that defines this association.

With this annotation in place, when persisting the three PostComment entities, we get the following SQL output:

insert into post (title, id) 
values ('First post', 1)

insert into post_comment (review, id) 
values ('My first review', 2)

insert into post_comment (review, id) 
values ('My second review', 3)

insert into post_comment (review, id) 
values ('My third review', 4)

update post_comment set post_id = 1 where id = 2

update post_comment set post_id = 1 where id =  3

update post_comment set post_id = 1 where id =  4

A little bit better, but what’s the purpose of those three update statements?

If you take a look at Hibernate flush order, you’ll see that the persist action is executed before the collection elements are handled. This way, Hibernate inserts the child records first without the Foreign Key since the child entity does not store this information. During the collection handling phase, the Foreign Key column is updated accordingly.

The same logic applies to collection state modifications, so when removing the firsts entry from the child collection:

post.getComments().remove(0);

Hibernate executes two statements instead of one:

update post_comment set post_id = null where post_id = 1 and id = 2

delete from post_comment where id=2

Again, the parent entity state change is executed first, which triggers the child entity update. Afterward, when the collection is processed, the orphan removal action will execute the child row delete statement.

So, is a java.util.Set any different?

No, it’s not. The same statements are executed if you use the @JoinColumn annotation on a unidirectional @OneToMany Set association.

Bidirectional @OneToMany

The best way to map a @OneToMany association is to rely on the @ManyToOne side to propagate all entity state changes:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(
        mappedBy = "post", 
        cascade = CascadeType.ALL, 
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();

    //Constructors, getters and setters removed for brevity

    public void addComment(PostComment comment) {
        comments.add(comment);
        comment.setPost(this);
    }

    public void removeComment(PostComment comment) {
        comments.remove(comment);
        comment.setPost(null);
    }
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    @GeneratedValue
    private Long id;

    private String review;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    //Constructors, getters and setters removed for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof PostComment )) return false;
        return id != null && id.equals(((PostComment) o).getId());
    }
    @Override
    public int hashCode() {
        return 31;
    }
}

There are several things to note on the aforementioned mapping:

  • The @ManyToOne association uses FetchType.LAZY because, otherwise, we’d fall back to EAGER fetching which is bad for performance.
  • The parent entity, Post, features two utility methods (e.g. addComment and removeComment) which are used to synchronize both sides of the bidirectional association. You should always provide these methods whenever you are working with a bidirectional association as, otherwise, you risk very subtle state propagation issues.
  • The child entity, PostComment, implement the equals and hashCode methods. Since we cannot rely on a natural identifier for equality checks, we need to use the entity identifier instead. However, you need to do it properly so that equality is consistent across all entity state transitions. Because we rely on equality for the removeComment, it’s good practice to override equals and hashCode for the child entity in a bidirectional association.

If we persist three PostComment(s):

Post post = new Post("First post");

post.addComment(
    new PostComment("My first review")
);
post.addComment(
    new PostComment("My second review")
);
post.addComment(
    new PostComment("My third review")
);

entityManager.persist(post);

Hibernate generates just one SQL statement for each persisted PostComment entity:

insert into post (title, id) 
values ('First post', 1)

insert into post_comment (post_id, review, id) 
values (1, 'My first review', 2)

insert into post_comment (post_id, review, id) 
values (1, 'My second review', 3)

insert into post_comment (post_id, review, id) 
values (1, 'My third review', 4)

If we remove a PostComment:

Post post = entityManager.find( Post.class, 1L );
PostComment comment1 = post.getComments().get( 0 );

post.removeComment(comment1);

There’s only one delete SQL statement that gets executed:

delete from post_comment where id = 2

So, the bidirectional @OneToMany association is the best way to map a one-to-many database relationship when we really need the collection on the parent side of the association.

Just @ManyToOne

Just because you have the option of using the @OneToMany annotation, it does not mean this should be the default option for every one-to-many database relationship. The problem with collections is that we can only use them when the number of child records is rather limited.

Therefore, in reality, @OneToMany is practical only when many means few. Maybe @OneToFew would have been a more suggestive name for this annotation.

As I explained in this StackOverflow answer, you cannot limit the size of a @OneToMany collection like it would be the case if you used query-level pagination.

Therefore, most of the time, the @ManyToOne annotation on the child side is everything you need. But then, how do you get the child entities associated with a Post entity?

Well, all you need is just a single JPQL query:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.post.id = :postId", PostComment.class)
.setParameter( "postId", 1L )
.getResultList();

Which translates to a straightforward SQL query:

select pc.id AS id1_1_,
       pc.post_id AS post_id3_1_,
       pc.review AS review2_1_
from   post_comment pc
where  pc.post_id = 1

Even if the collection is not managed anymore, it’s rather trivial to just add/remove child entities whenever necessary. As for updating child objects, the dirty checking mechanism works just fine even if you don’t use a managed collection. What’s nice about using a query is that you can paginate it any way you like so that, if the number of child entities grows with time, the application performance is not going to be affected.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

As you will see in a future article, bidirectional collections are way better than unidirectional ones because they rely on the @ManyToOne association, which is always efficient in terms of generated SQL statements.

But then, even if they are very convenient, you don’t always have to use collections. The @ManyToOne association is the most natural and also efficient way of mapping a one-to-many database relationship.

FREE EBOOK

58 Comments on “The best way to map a @OneToMany relationship with JPA and Hibernate

  1. If i use @ManyToOne association on child entity only as you mentioned at the last of article as hint, like mentioned below
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = “post_id”)
    private Post post;

    and remove below code of List association from parent entity
    @OneToMany(
    mappedBy = “post”,
    cascade = CascadeType.ALL,
    orphanRemoval = true
    )
    private List comments = new ArrayList();

    so then how will i maintain cascading on parent entity like whenever parent entity delete all it’s children should be and whenever we remove relationship of child to it’s parent then child should be deleted.

  2. Hi, you are showing the executed SQL queries for the Java snippets. But I’m wondering, where the auto-incremented IDs for posts and post comments come from. I mean the following the ones:

    Hibernate generates just one SQL statement for each persisted PostComment entity:

    insert into post (title, id)
    values (‘First post’, 1)

    insert into post_comment (post_id, review, id)
    values (1, ‘My first review’, 2)

    Shouldn’t it be more like:

    insert into post (title)
    values (‘First post’)

    And then Hibernate might figure out the newly assigned auto-incremented IDs and use them for the post comments like:

    insert into post_comment (post_id, review)
    values (1, ‘My first review’)

    Or am I’m missing some detail here? 🙂

    Best Regards

    • Sure I can help you with your problem. After analyzing it, I realized that a consulting session of 2 or 3 hours will be needed in order to provide an answer to your question. If you’re interested, let me know, and I’ll send you a consulting contract.

  3. I am a little afraid about the performance when we are modelling our One-To-Many relationship as a bidirectional.

    Let’s say have a Person that may contain many adresses:

    Person
    @OneToMany(fetch = LAZY, mappedBy=”person”)
    List addresses

    Address
    @ManyToOne(fetch = LAZY)
    Person person;

    Following your hints, we should always keep or objects in sync, so let’s imagine we want to remove address from a person:
    Address address = …;
    Person person = …;
    person.getAddresses().remove(address);

    Even though we have annotated address list as lazy and we do not need it, once we do getAddresses().remove(…) it is read from DB anyway. What if there are one million addresses assigned to the person? Is there anything that could be done to make it better? I guess this is what you meant by “OneToFew” 🙂

    Of course we could use your last hint, but then we kind of lose nice and easy to use Object model.

    • You don’t need to call the synchronized methods if you only operate on child entities in the current running Persistence Context. Those are needed if both the parent and the children are needed.

      • But in that case when I am invoking
        addressRepository.delete(address) nothing happens

      • I fail to understand why calling delete would not work in this case. Try to debug it and see why is it so.

  4. Thanks for the explanation. I am running into an issue when combining a bidirectional association with a foreign key being part of an EmbeddedId of the child association. Whenever adding childs to an existing parent, the merge strategy never finds existing records since the foreign key is always null.

    Please see my SO post for more details: https://stackoverflow.com/questions/56566827/hibernate-bidirectional-association-with-embeddedid-foreign-key-is-null-when-me

    • Maybe it’s a Hibernate bug. If you can replicate it with a trst case, you need to open a Hibernate Jira issue.

      • I found I could work around it by making the setProject() not only set the project of the child record, but also manually set the project_id in the @EmbeddedId object to the id of the given project. (if either are non-null).

        This seems a bit cumbersome, though. Isn’t this exactly what @MapsId(“projectId”) should do?

        Before opening a bug for Hibernate, I first want to make sure I am not misusing the tools available.

      • Actually, you need to keep the id and the association in sync. MapsId is only for fetching, not for updates.

      • Ah, thanks! I need to do that because we are dealing with an @EmbeddedId which contains this project_id as part of its key? In your Post-Comment example you don’t have an explicit post_id attribute in PostComment, so for you setting just the ‘post’ object is enough for Hibernate to do the mapping under the hood?

        So then indeed I need to set it as part of the setProject method in ProjectShare?

        Do you still think this is a shortcoming of Hibernate or common practice? I am surprised I was not able to find this very common use case in any documentation or SO post.

        Thanks for the quick replies.

      • Thank you for fast answer.

        My new question is: why in official new Hibernate documentation in section – “2.7.2. @OneToMany” – is only shown Unidirectional @OneToMany default/worst (according this article) way of implementation ? There isn’ t in section “2.7.2. @OneToMany” what implementation is prefered? and in section “OneToMany” isnt wrote better (according this article) example with @JoinColumn?

        It looks that Your article is more complex than official documentation.
        And new official documentation looks not very useful because after section – “2.7.2. @OneToMany” I know only bad @OneToMany implementation and I don’t know which is best or prefered.

      • The Hibernate documentation is on GitHub, so you can submit a Pull Request with a fix for it if you’d like to make it better.

  5. Hi Vlad,

    This is great stuff. Thanks a lot for posting this!

    Question,
    What is your opinion on avoiding all those relationship annotations (@OneToMany, @ManyToOne, @OneToOne) when using JPA/Hibernate?

    Thank you!

      • Sure I will! Do your video tutorials (mach 1/2) cover this topic as well? – Thanks.

      • Yes, of course. The video course is the most effective way to acquire all this knowledge.

      • Cool. Enjoy watching the videos!

  6. Hi,

    Can someone help me here with the following issue:

    I have one side of the relation:
    @OneToMany(mappedBy = “id.address”, fetch = FetchType.LAZY)
    public Set getPartyAddress() {
    return partyAddress;
    }
    The other side I am using @EmbeddedId with @Embeddable.

    I am getting the following error when doing enhacement:

    Unable to enhance the field field [id.address]

  7. hi i am using the same structure for one to many mapping as you have specified. My parent table is Feeds table and child table is Comments table. I am able to create new feeds with list of comments but when i am adding one more comment using the same feed id i am getting exception as

    could not execute batch; SQL [insert into FEEDS_COMMENTS_MAPPING (FEED_ID, COMMENT_ID) values (?, ?)]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException

    Can u please explain why and solution for adding the comments for the same feed.Thanks

  8. If the client send a Post object with a list of comments in it, how can you save the post with all the comments in it when using @ManyToOne on the child only?

    • Like this: post.getComments().stream().forEach(EntityManager::persist)

  9. Hello,
    I am using Uni-directional OneToMany relationship (Employee to Address) in my project. When I run it I see extra column (with name address_id) being created on Child (Address) table. I belive it’s foreign key, but it should be Employee rather than Address table

    @Entity
    @Table(name = "employee")
    @Data
    public class Employee {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
    	@Column(name = "first_name")
    	private String firstName;
    
    	@Column(name = "last_name")
    	private String lastName;
    
    	@Column(name = "email")
    	private String email;
    
    	@Column(name = "phone")
    	private String phone;
    
    	@OneToMany(cascade = CascadeType.ALL)
    	@JoinColumn(name = "address_id",referencedColumnName = "id")
    	private List<Address> addresses = new ArrayList<>();
    
    }
    
    @Entity
    @Table(name = "address")
    @Data
    public class Address {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
    	@Column(name = "street_name")
    	private String streetName;
    
    	@Column(name = "apartment")
    	private String apartment;
    
    	@Column(name = "city")
    	private String city;
    
    	@Column(name = "zip_code")
    	private String zipCode;
    
    }
    
  10. If we look at the (redacted) post entity: mappedBy=”post”, @Entity(name=”Post”), @Table(name=”post”).

    Is it correct to assume that the mappedBy=”post” refers to the field ‘post’ in the other entity?
    So it is referring to this:

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
    

    Because the name of the field is ‘post’?

    • It references the entity persistent attribute, which can be mapped via a field or a JavaBean property.

  11. I followed the bidirectional example and all is good, the postcomments appear as a set when i get one Post. But if i make a get request for a postcomment and don’t apply JsonIgnore to getPost() method i run into a recursive hell. The problem is, i need the connection between Post Comment and Post, i need that post_id.

    Kind regards,

    • Yes, you need to use @JsonIgnore, but not because of Hibernate. It’s because of Jackson. Also, the excessive use of entities for read-only views is a code smell. You are better off using DTOs. Check out my High-Performance Java Persistence book for more details.

  12. Thank you Vlad for great blog

    However I have one question, for me it seems that older comments aren’t visible. Is it so?

    • Thanks. Yes, comments have a one year validity, after which they get deleted to make room for new comments.

  13. I tried the bi-directional mapping with a Set, and receive this error when trying to remove a comment:

    org.hibernate.StaleStateException: Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1

    If I take away the comment.setPost(null), it works fine. Is this expected behavior or have I made another mistake somewhere?

  14. Excellent article.
    I have one question on this. Working on a project that is more of transforming existing PB application to Java. DB is Oracle and this app has been running for last 20+ years. Since this is a transformation project and since both PB and Java need to co-exist for some time, we are not in a position to change anything in DB.
    We have classes like these
    Customer.java {
    //Other fields
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = “p_object”, referencedColumnName = “uoid”)
    private List addressList;

    }

    Organization.java {
    //Other fields
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = “p_object”, referencedColumnName = “uoid”)
    private List addressList;
    }

    Address.java {

    @Column(name="p_object")
    private String parent;

    @Column(name="p_object_cls")
    private String parentObjectClass;

    }
    Since Address could have either Organization or Customer as parent, existing app stores primary key of the parent in p_object column and the type of the object(Organization or Customer) in p_object_cls column
    As you mentioned in your article we are seeing insert and update queries for each insert into update table
    Is there any way we can change this to ManyToOne relationship considering parent object types could be different?

  15. Hi,
    this is working in your case but in my case this way of mapping doesn’t work. I have :
    @Entity
    @Table(name = “apartment”)
    public class ApartmentEntity extends AbstractEntity {

    @Column(nullable = false)
    private Integer price;

    @Enumerated(EnumType.STRING)
    private ApartmentStatus apartmentStatus;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "building_id")
    private BuildingEntity building;

    and:
    @Entity
    @Table(name = “building”)
    public class BuildingEntity extends AbstractEntity {

    private String description;

    @OneToMany(mappedBy = "building", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ApartmentEntity> apartments;

    and this is not working becasue when I in Postman call building id 2, it has tree apartments (and one is apartmen id 2) and this is ok, but when I call apartment id 2, this apartment should have building id2 but from Postam I see JSON with building as a null. Do you know why?

  16. Did I missed anything or what is wrong?

    java.lang.IllegalArgumentException: org.hibernate.hql.internal.ast.QuerySyntaxException: POST_COMMENTS is not mapped [SELECT pc FROM POST_COMMENTS pc WHERE pc.post.post_id = :postId]

    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:138)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:713)
    at org.hibernate.internal.AbstractSharedSessionContract.createQuery(AbstractSharedSessionContract.java:729)
    at org.hibernate.internal.AbstractSessionImpl.createQuery(AbstractSessionImpl.java:23)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)

    TABLE:
    create table POST_COMMENTS
    (
    pc_id bigint auto_increment,
    pc_review varchar(60) null,
    pc_post_id bigint null,
    constraint POST_COMMENTS_pc_id_uindex
    unique (pc_id)
    );

    alter table POST_COMMENTS
    add primary key (pc_id);

    unitTest:
    @Test
    public void testSelectWithJpqlQuery() {
    java.util.List comments = entityManager.createQuery(
    “SELECT pc ” +
    ” FROM POST_COMMENTS pc ” +
    ” WHERE pc.post.post_id = :postId”, PostComments.class)
    .setParameter( “postId”, 7L )
    .getResultList();
    System.out.print(” ============== printing ===================\n\n”);
    int count=1;
    for (PostComments c: comments) {
    System.out.print(count+”. “+c.getPcId()+”, “+c.getPcReview()+”, with post=\n”+c.getPost().toString()+”\n\n”);
    count++;
    }

    }

    • You used the table name instead of the entity name in your JPQL query.

  17. Hello Vlad, first of all, I really appreciate your content on this blog.

    I actually have one question related to One to Many bidirectional relationship. What is the best practice when we do not actually want to remove the child when the parent is removed ?

  18. In bidirectional mapping, if I use that code, we will have any differences or not? I see same result in two cases. Please help me explain:

    Post post = new Post(“First post”);
    entityManager.persist(post);

    post.addComment(
    new PostComment(“My first review”)
    );
    post.addComment(
    new PostComment(“My second review”)
    );
    post.addComment(
    new PostComment(“My third review”)
    );

  19. Thanks for this.

    When using bidirectional @OneToMany and adding child records, is there a generic way to set the child’s reference to the parent assuming you have both instances but don’t know the class at compile time?

    I’m considering looking up the parent’s @OneToMany annotation at runtime, getting its mappedBy value, then using reflection on the child to set the parent into it… but I suspect this might have already been done somewhere?

      • Thanks. Is that addComment()?

        I don’t have methods like that for each Set – the Entity classes are reverse engineered by HibernateTools.
        If I did, at runtime I’d have to call it by reflection so would have to work out the method name from the property name.

        Hence I’m considering the mappedBy lookup and reflection described above.

      • I’ve never used Hibernate Tools, but I suppose it’s better to add those utility methods anyway.

      • I guess that means an auto/generic way to do it does not yet exist 🙂

        Hibernate-tools is part of our CI pipeline, we prefer designing the data model in SQL and making that the single source of truth.

        It would be more effort to add addXXX() method generation to hibernate-tools but that would save on reflection at runtime.

      • Then maybe it’s contributing that feature to the Hibernate Tools project.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.