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

(Last Updated On: January 29, 2018)

Introduction

There are many ways you can map a one-to-one relationship with Hibernate. In this post, I’m going to demonstrate which mapping is the most efficient one from a database perspective.

Domain Model

For the following examples, I’m going to use the following Post and PostDetails classes:

OneToOne

The Post entity is the parent, while the PostDetails is the child association because the Foreign Key is located in the post_details database table.

Typical mapping

Most often, this relationship is mapped as follows:

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

    @Id
    @GeneratedValue
    private Long id;

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

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

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

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

More, even the Post entity can have a PostDetails mapping as well:

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

    @Id
    @GeneratedValue
    private Long id;

    private String title;

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

    //Getters and setters omitted for brevity

    public void setDetails(PostDetails details) {
        this.details = details;
        details.setPost(this);
    }
}

However, this mapping is not the most efficient, as further demonstrated.

The post_details table contains a Primary Key (PK) column (e.g. id) and a Foreign Key (FK) column (e.g. post_id).

one-to-one

However, there can be only one post_details row associated with a post, so it makes more sense to have the post_details PK mirroring the post PK.

one-to-one-shared-pk

This way, the post_details Primary Key is also a Foreign Key, and the two tables are sharing their PKs as well.

PK and FK columns are most often indexed, so sharing the PK can reduce the index footprint by half, which is desirable since you want to store all your indexes into memory to speed up index scanning.

While the unidirectional @OneToOne association can be fetched lazily, the parent-side of a bidirectional @OneToOne association is not. Even when specifying that the association is not optional and we have the FetchType.LAZY, the parent-side association behaves like a FetchType.EAGER relationship. And EAGER fetching is bad.

This can be easily demonstrated by simply fetching the Post entity:

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

Hibernate fetches the child entity as well, so, instead of only one query, Hibernate requires two select statements:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_
FROM   post p
WHERE  p.id = 1

SELECT pd.post_id AS post_id3_1_0_, pd.created_by AS created_1_1_0_,
       pd.created_on AS created_2_1_0_
FROM   post_details pd
WHERE  pd.post_id = 1

Even if the FK is NOT NULL and the parent-side is aware about its non-nullability through the optional attribute (e.g. @OneToOne(mappedBy = "post", fetch = FetchType.LAZY, optional = false)), Hibernate still generates a secondary select statement.

For every managed entity, the Persistence Context requires both the entity type and the identifier,
so the child identifier must be known when loading the parent entity, and the only way to find the associated post_details primary key is to execute a secondary query.

Bytecode enhancement is the only viable workaround. However, it only works if the parent side is annotated with @LazyToOne(LazyToOneOption.NO_PROXY) and the child side is not using @MapsId.

The most efficient mapping

The best way to map a @OneToOne relationship is to use @MapsId. This way, you don’t even need a bidirectional association since you can always fetch the PostDetails entity by using the Post entity identifier.

The mapping 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;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

This way, the id column serves as both Primary Key and FK. You’ll notice that the @Id column no longer uses a @GeneratedValue annotation since the identifier is populated with the identifier of the post association.

The PostDetails entity can be persisted as follows:

doInJPA(entityManager -> {
    Post post = entityManager.find(Post.class, 1L);
    PostDetails details = new PostDetails("John Doe");
    details.setPost(post);
    entityManager.persist(details);
});

And we can even fetch the PostDetails using the Post entity identifier, so there is no need for a bidirectional association:

PostDetails details = entityManager.find(
    PostDetails.class, 
    post.getId()
);

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

Conclusion

Knowing how to map entity relationships efficiently can make a lot of difference when it comes to application performance. For @OneToOne associations, you should always share the Primary Key with the parent table, and you should avoid the bidirectional association if you don’t plan to use bytecode enhancement.

Code available on GitHub.

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

82 thoughts on “The best way to map a @OneToOne relationship with JPA and Hibernate

  1. Hi vlad,
    Thanks for the article.
    In my case, I have User and UserDetail Entities, unidirectional 1to1.

    In Userdetail, I have following code which is resulting in following error during Userdetails saving.

        @OneToOne(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
        @MapsId
        private User user;
    
    Caused by: org.postgresql.util.PSQLException: ERROR: column "user_id" of relation "user_details" does not exist
    

    Only adding additional annotation @JoinColumn(name="id") is resolving the error.

    What I assume is if I do @MapsId, Hibernate should bind the id automatically.
    Is that correct? Why I am needing @JoinColumn(name="id")?

    Regards,
    Mohan

    1. Because that’s the way Hibernate works.

      The FK is taken from the user property and becomes the column: user_id while the id is simply ignored and populated with the FK value.

      So, adding a @JoinColumn is the JPA-way of defining how you want to name the underlying shared PK and FK column.

      1. Thank you very much for your quick response.
        So what I understand is, everytime I do @MapsId I always need to define @JoinColumn(name=”primary_key_column_name_to_be_shared”)

        But I am wondering why the example from your post under section The most efficient mapping example works without adding @JoinColumn.

      2. You don’t need to provide that if you are fine having the PK column name as property_id.

  2. Hello Vlad,
    How to delete a Post in your latest configuration if there is a related PostDetails? Is it possible to use CascadeType.REMOVE in this case to avoid manual removing in the code?

    1. In the absence of a bidirectional association, you can set up FK-level ON DELETE cascade. This way, you can just delete the parent and the DB will remove the child row as well.

  3. Hi Vlad,
    Maybe I am missing something or not understanding it correctly. From the code on GitHub, it looks like your Post entity never has a PostDetails property. If that is true, then you would have to get a PostDetails object in order to have all the information for a Post available in one object (since PostDetails does have a Post property). This seems backwards to me. I would think that a Post would give you everything, and PostDetails would only have the details of a post. This would particularly be true if a Post did not have to have a PostDetail. With the approach you have presented, then I would either have to grab a Post (if no PostDetail existed) or a PostDetail (if one did exist) to get all the information about a post in a single object. Is my thinking regarding this just not correct? Thanks!

    1. On GitHub, you will find lots of tests for both unidirectional and bidirectional associations. I suppose you only checked the unidirectional one. There’s one for bidirectional @OnetoOne.

      This seems backwards to me. I would think that a Post would give you everything, and PostDetails would only have the details of a post.

      It depends on the Domain Model. There are many ways to model this relationship.

      This would particularly be true if a Post did not have to have a PostDetail.

      A Post does not have to have a PostDetails. Just check the database table and you’ll see that the parent has no idea that it has any child tables associated with it.

      With the approach you have presented, then I would either have to grab a Post (if no PostDetail existed) or a PostDetail (if one did exist) to get all the information about a post in a single object

      You can always get the entire information like this:

      select p, pd
      from Post p
      left join PostDetaild pd on p = pd.post
      where p.id = :postId
      

      This will work even if you have a unidirectional association and there is no associated PostDetails entity.

      1. Thanks for your clarifications. I think I’m looking for a uni-directional one-to-one, but going the other way. In other words, the PostDetails would not have a Post property, but the Post would have a PostDetails property. Is that possible?

        I think I could also do it with a bi-directional relationship, but am I correct that in doing so I would have to have a unique primary key for the PostDetail? (I can’t use the primary key from Post as the primary key in PostDetail?)

        With the way that you have it, I understand that you can get all the information like the query you showed. However, you still have two objects (p & pd). I’m looking to have one Post object that has all the information for the Post and the PostDetail if it exists.

      2. If you need that, then maybe it’s better to not split the info in 2 tables and have everything in Post.

  4. Hello Vlad,

    Thanks for great article. A colleague of mine altered your example code and asked me why shouldn’t we do it like this

    @Entity
    @Table(name = "post_details")
    public class PostDetails {
    
        @Id
        @GeneratedValue
        private Long id;
    
        @OneToOne(mappedBy = "details", optional = false)
        @JoinColumn(name = "post_id")
        private Post post;
    
        public PostDetails() {}
    }
    
    
    @Entity
    @Table (name = "post")
    public class Post {
        @Id
        @GeneratedValue
        private Long id;
    
        @OneToOne(cascade = CascadeType.ALL)
        private PostDetails details;
    
        public void setDetails(PostDetails details) {
            this.details = details;
            // details.setPost(this); not setting Post in PostDetails
          }
    }
    
    

    Obviously this example deviates from id being both PK and FK (that would a step toward efficient mapping as you suggested).

    The argument put forward by my colleague is; this way,
    1. Only persistence of Post is enough, as post may or may not have PostDetails, why not build PostDetails while building a post if PostDetails exists otherwise PostDetails in NULL

    PostDetails details = new PostDetails();
    Post post = new Post();
    post.setDetails(details);
    
    entityManager.persist(post);
    

    While finding a Post (e.g. based on Post id) PostDetails are fetched as well if available. There is no need to use id of Post explicitly and fetch PostDetails if any.

    I am flabbergasted.

    Q1. Is this a good/bad design, I would really like to know why?
    Q2. Why do mappedBy = "details" in PostDetails works?

    Thanks,

    1. That’s not really correct. The FK should be on the child side, and here, you added it on the Parent side.

      In your design, you can have a PostDetails without a Post, which is not correct.

      The mappedBy works on any side because it’s just a way to tell Hibernate that the other end controls the associations.

Leave a Reply

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