How to synchronize bidirectional entity associations with JPA and Hibernate

(Last Updated On: May 9, 2018)

Introduction

While answering this StackOverflow question, I realized that it’s a good idea to summarize how various bidirectional associations should be synchronized when using JPA and Hibernate.

Therefore, in this article, you are going to learn how and also why you should always synchronize both sides of an entity relationship, no matter if it’s @OneToMany, @OneToOne or @ManyToMany.

One-To-Many

Let’s assume we have a parent Post entity which has a bidirectional association with the PostComment child entity:

The PostComment entity looks as follows:

@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;

    //Getters and setters omitted 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).id);
    }
    @Override
    public int hashCode() {
        return 31;
    }
}

There are several things to notice in the PostComment entity mapping above.

First, the @ManyToOne association uses the FetchType.LAZY strategy because by default @ManyToOne and @OneToOne associations use the FetchType.EAGER strategy which is bad for performance.

Second, the equals and hashCode methods are implemented so that we can use safely use the entity identifier, as explained in this article.

The Post entity is mapped as follows:

@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<>();

    //Getters and setters omitted for brevity

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

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

The comments @OneToMany association is marked with the mappedBy attribute which indicates that the @ManyToOne side is responsible for handling this bidirectional association.

However, we still need to have both sides in sync as otherwise, we break the Domain Model relationship consistency, and the entity state transitions are not guaranteed to work unless both sides are properly synchronized.

For this reason, the Post entity defines the addComment and removeComment entity state synchronization methods.

So, when you add a PostComment, you need to use the addComment method:

Post post = new Post();
post.setTitle("High-Performance Java Persistence");

PostComment comment = new PostComment();
comment.setReview("JPA and Hibernate");
post.addComment(comment);

entityManager.persist(post);

And, when you remove a PostComment, you should use the removeComent method as well:

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

post.removeComment(comment);

For more details about the best way to map a @OneToMany association, check out this article.

One-To-One

For the one-to-one association, let’s assume the parent Post entity has a PostDetails child entity as illustrated in the following diagram:

The child PostDetails entity looks like this:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;
    
    //Getters and setters omitted for brevity
}

Notice that we have set the @OneToOne fetch attribute to FetchType.LAZY, for the very same reason we explained before. We are also using @MapsId because we want the child table row to share the Primary Key with its parent table row meaning that the Primary Key is also a Foreign Key back to the parent table record.

The parent Post entity looks as follows:

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

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    @OneToOne(
        mappedBy = "post", 
        cascade = CascadeType.ALL, 
        orphanRemoval = true, 
        fetch = FetchType.LAZY
    )
    private PostDetails details;

    //Getters and setters omitted for brevity

    public void setDetails(PostDetails details) {
        if (details == null) {
            if (this.details != null) {
                this.details.setPost(null);
            }
        }
        else {
            details.setPost(this);
        }
        this.details = details;
    }
}

The details @OneToOne association is marked with the mappedBy attribute which indicates that the PostDetails side is responsible for handling this bidirectional association.

The setDetails method is used for synchronizing both sides of this bidirectional association and is used both for adding and removing the associated child entity.

So, when we want to associate a Post parent entity with a PostDetails, we use the setDetails method:

Post post = new Post();
post.setTitle("High-Performance Java Persistence");

PostDetails details = new PostDetails();
details.setCreatedBy("Vlad Mihalcea");

post.setDetails(details);

entityManager.persist(post);

The same is true when we want to dissociate the Post and the PostDetails entity:

Post post = entityManager.find(Post.class, 1L);

post.setDetails(null);

For more details about the best way to map a @OneToOne association, check out this article.

Many-To-Many

Let’s assume the Post entity forms a many-to-many association with Tag as illustrated in the following diagram:

The Tag is mapped as follows:

@Entity(name = "Tag")
@Table(name = "tag")
public class Tag {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String name;

    @ManyToMany(mappedBy = "tags")
    private Set<Post> posts = new HashSet<>();

    //Getters and setters omitted for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) 
            return true;
            
        if (!(o instanceof Tag))
            return false;
        
        Tag tag = (Tag) o;
        return Objects.equals(name, tag.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

Notice the use of the @NaturalId Hibernate-specific annotation which is very useful for mapping business keys.

Because the Tag entity has a business key, we can use that for implementing equals and hashCode as explained in this article.

The Post entity is then mapped as follows:

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

    @Id
    @GeneratedValue
    private Long id;

    private String title;

    public Post() {}

    public Post(String title) {
        this.title = title;
    }

    @ManyToMany(
        cascade = { 
            CascadeType.PERSIST, 
            CascadeType.MERGE
        }
    )
    @JoinTable(name = "post_tag",
        joinColumns = @JoinColumn(name = "post_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private Set<Tag> tags = new LinkedHashSet<>();

    //Getters and setters omitted for brevity   

    public void addTag(Tag tag) {
        tags.add(tag);
        tag.getPosts().add(this);
    }

    public void removeTag(Tag tag) {
        tags.remove(tag);
        tag.getPosts().remove(this);
    }

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

    @Override
    public int hashCode() {
        return 31;
    }
}

The tags @ManyToMany association is marked with the mappedBy attribute which indicates that the Tag side is responsible for handling this bidirectional association.

The addTag and removeTag methods are used for synchronizing the bidirectional association. Because we rely on the remove method from the Set interface, both the Tag and Post must implement equals and hashCode properly. While Tag can use a natural identifier, the Post entity does not have such a business key. For this reason, we used the entity identifier to implement these two methods, as explained in this article.

To associate the Post and Tag entities, we can use the addTag method like this:

Post post1 = new Post("JPA with Hibernate");
Post post2 = new Post("Native Hibernate");

Tag tag1 = new Tag("Java");
Tag tag2 = new Tag("Hibernate");

post1.addTag(tag1);
post1.addTag(tag2);

post2.addTag(tag1);

entityManager.persist(post1);
entityManager.persist(post2);

To dissociate the Post and Tag entities, we can use the removeTag method:

Post post1 = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "join fetch p.tags " +
    "where p.id = :id", Post.class)
.setParameter( "id", postId )
.getSingleResult();

Tag javaTag = entityManager.unwrap(Session.class)
.bySimpleNaturalId(Tag.class)
.getReference("Java");

post1.removeTag(javaTag);

For more details about the best way to map a @ManyToMany association, check out this article.

That’s it!

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

Conclusion

Whenever you are using a bidirectional JPA association, it is mandatory to synchronizing both ends of the entity relationship.

Not only that working with a Domain Model, which does not enforce relationship consistency, is difficult and error prone, but without synchronizing both ends of a bidirectional association, the entity state transitions are not guaranteed to work.

So, save yourself some trouble and do the right thing.

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence, 
  • a 10% discount coupon for my book. 
Get the most out of your persistence layer!

Advertisements

9 thoughts on “How to synchronize bidirectional entity associations with JPA and Hibernate

  1. Hi Vlad,

    Thank you for your blog.
    Should we prevent the modification of the relationship from the other side?

    Means in case of the above Many-To-Many example, should we prevent to persist a Tag object after setting posts to it. (i.e. tag1.setPosts(posts))

      1. So I was just asking if persisting Tag, like below, is ok or not?

        Post post1 = new Post("JPA with Hibernate");
        Post post2 = new Post("Native Hibernate");
        Set<Post> posts = new HashSet<>();
        posts.add(post1);
        posts.add(post2);

        Tag tag = new Tag("Hibernate");
        tag.setPosts(posts);

        entityManager.persist(tag)

      2. It’s ok only if you have a unidirectional association between Tag and Post. For a bidirectional association, it’s not ok, as explained in the article.

  2. LINK: https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/

    Hello Vlad,

    thanks for sharing another helpful JPA tip which helps summarize the use of JPA to manage “aggregate root” entity objects.

    I recently came across many related articles on the topic of “aggregate root” entity objects and derived a solution very similar to your code snippets.

    I found the following articles helpful and they may be helpful to other readers of your JPA tips:

    Advancing Enterprise DDD – The Entity and the Aggregate Root
    ** http://scabl.blogspot.com.au/2015/03/aeddd-5.html
    Domain Driven Design: Entities, Value Objects, Aggregates and Roots with JPA (Part 1)
    ** https://simbo1905.blog/2016/07/18/domain-driven-design-entities-value-objects-aggregates-and-roots-with-jpa-part-1/
    Domain Driven Design: Entities, Value Objects, Aggregates and Roots with JPA (Part 2)
    ** https://simbo1905.blog/2016/07/18/domain-driven-design-entities-value-objects-aggregates-and-roots-with-jpa-part-2/
    Domain Driven Design: Entities, Value Objects, Aggregates and Roots with JPA (Part 3)
    ** https://simbo1905.blog/2016/07/18/domain-driven-design-entities-value-objects-aggregates-and-roots-with-jpa-part-3/

    Keeping posting your JPA tips; they are helpful for JPA development.

    Regards,

    Tim

    p.s. Yes, I have a copy of your JPA book.

  3. Very good post, Vlad!

    Most of the time I try to avoid bidirectional relationships as much as possible, although I understand their benefits for performance in some scenarios. Well, for me, configuring bidirectional associations depends on more the domain than app’s schema itself.

    Even with the use of the entity’s methods [addChild() and removeChild()], keeping both sides in sync is still hard and tricky without automated tests. But you know, the best way to avoid those kind of issues is educating the team on the good practices!

  4. I always enjoy and learn from your writings, so thank you.
    One (ok, maybe 2) question I have regarding the equals is the use of instanceof versus getClass. In the Tag class you use the latter, in the Post class the former.
    Shouldn’t the ‘instanceof’ way be favored since the class could be proxied and hence the getClass could return false?
    And for the same reason (proxied objects) should I prefer the getter over field access?

      1. instanceof vs getClass matter only if you plan to extend the entity or if you use Proxies, otherwise it doesn’t really matter too much. I guess instanceof is more reliable for Proxies.
      2. Field access is a better choice. You don’t have to use @Transient for extra methods and works better with Proxies.

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.