The best way to map a @OneToOne 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

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) {
        if (details == null) {
            if (this.details != null) {
                this.details.setPost(null);
            }
        }
        else {
            details.setPost(this);
        }
        this.details = details;
    }
}

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 property serves as both Primary Key and Foreign Key. 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.

If you want to customize the Primary Key column name when using @MapsId, you need to use the @JoinColumn annotation. For more details, check out this article.

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.

Seize the deal! 40% discount. Seize the deal! 40% discount.

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.

Transactions and Concurrency Control eBook

26 Comments on “The best way to map a @OneToOne relationship with JPA and Hibernate

  1. if you leave only unidirectional mapping you won’t be able to navigate in a jpql query.
    For example if you have a User with one to many to Post and want something like
    Select u from User u join fetch u.posts p where p.postDetails.”rating or smth else” = 5
    so i think you make it a biderectional will @MapsId still work? or there is no point to use @MapsId with a biderectional oneToone mapping?

    • Of course, you can. Just change your query to this:

      select u
      from PostDetails pd
      join pd.post p
      join p.user u
      join fetch u.posts
      where pd.rating = 5
      
      • Vlad,
        exellent answer from the expert as always!
        a minor note it seems that the query needs distinct.

        And one more remark, as i can see from logs with my first example
        Select u from User u join fetch u.posts p where p.postDetails.rating = 5
        and bidirectional mapping
        Hiberanate generates n+1 additional selects to get postdetails BUT each user’s posts collection will contain only posts with rating 5 as a desirable outcome.
        And with your variant each user’s collection will contain all posts regardless of rating
        but no additional queries to get postdetails. So it’s more like i’m filtering only users and not thier posts also, like in my first case.
        So it seems like it depends on use case when to use a bidirectional or unidirectional mapping.
        or maybe I did a mistake somewhere….

  2. Hello. When I tried it I got an error saying “Detached entity passed to persist”. How can I fix this?

  3. Hi Vlad, thank you for your article. I am new to Java and JPA and your articles have been helpful. I was wondering how would the case be if the child table had another OneToOne relationship with another table?

  4. Hey Vlad, great article. I was wondering if there is anyway to MapsId in such a way that by default, the parent lazily loads the child OneToOne relationship but if a NamedEntityGraph is specified, then a join is used to get both the parent and the child.

    If I’m not mistaken, with MapsId, you’ve completely removed the child reference from the parent because now you can get the child using the parent’s ID. So for cases where you know ahead of time that you want the child, how would you tell hibernate to get all the data in one query (with NamedEntityGraph or something else)?

    • If you map only the child side only, then you get the default lazy loading. And to fetch both associations, do something like this:

      select pd
      from PostDetails pd
      join fetch pd.post
      
      • Ok I think I get it. One more thing, how would you handle cascading without the parent having a reference to the child? Ideally you could do post.setDetails(new PostDetails(…)) and then update post and have PostDetails be automagically added as well. Is this possible?

      • It depends on the cascading type. And, not all associations need to be updated together. In the worst case, you have to call persist/merge twice instead of once.

      • I think persisting would be fine to do in separate calls, I’m just worried that if the parent gets deleted then there will be a detached child object. So at the very least, I want to have cascade for removal.

        To do this, I would need a reference to the child in the parent object with a mappedBy which would break the laziness if I’m not mistaken. So how could you cascade removal with OneToOne and keep it lazy?

  5. During reading this article and your book I’ve noticed that annotations like @OneToOne, @OneToMany … sometimes come in pair with @JoinColumn. The book even states that @JoinColumn may increase performance in some cases. But during researching the internet I haven’t managed to find explanation of real purpose of @OneToOne annotation and the real purpose of @JoinColumn annotation. On stackoverflow I even saw that you can ommit @JoinColumn if your ORM-DB naming matches and it will be automatically generated. I really cannot understand what hibernate does internally when sees @OneToOne annotation and what hibernates does additionally internally when sees @JoinColumn annotation.

    • JoinColumn does not increase performance. It only allows a unidirectional association to avoid the link table. But, you shouldn’t use unidirectional @OneToMany anyway.

      • Okay, thanks, but what is the purpose of JoinColumn for OneToOne? OneToOne does not have a junction table.

      • JoinColumn is just to specify the FK column explicitly. Otherwise, Hibernate will use a default column name.

  6. Hello Vlad, I’ve purchased your book High-Performance Java Persistence book and it’s really amazaing but I couldn’ find the answer there why we cannot use unidirectional behaviour but by simply storing PostDetail field in Post object. And not store Post field in PostDetail object. So our dbs would seem like Post(id, title, post_details_id) and PostDetails (id, createdOn, createdBy).

    • It’s all explained in the @OneToOne chapter.

      First of all, your table relationship are not one-to-one. You are using a one-to-many table relationship that happens to have a unique constraint on the post_id column. That’s not the proper way of designing this relationship. You should simply share the PKs. It’s even worse if you decide to add the FK in the parent table because then the post_details need to be stored prior to its parent. Not to mention that you can delete a post and have a floating post_details that’s not associated to any post.

      • Thanks a lot for a quick reply, but :

        1) We could specify cascade an all operation of FK post_details in parent table and prevent floating post_details
        2) My example that I am trying to implement is ProductEntity and ProductDescriptionEntity,
        relationship should be OneToOne but I always have to be able to fetch product description from product entity. That’s why I am wondering why I cannot store product description FK in product with lazy fetch type and cascateType = all ? And product description would know nothing about product

      • 1) You would still waste space with an extra column and index that’s not needed.
        2) You need to use MapsId if you want a proper one-to-one relationship. If you do that, fetching the ProductDescriptionEntity is as easy as that:

        ProductDescriptionEntity desc = entityManager.find(ProductDescriptionEntity.class, product.getId());

        Because booths entities share the same id, you can easily do that.

        That’s why I am wondering why I cannot store product description FK in product with lazy fetch type and cascateType = all ? And product description would know nothing about product

        You can do that, but it would be a mistake as it violates the principles of relational database modeling. If the product_description cannot exist without a product, then product is the parent, not the child. And FKs are added in child tables, not in the parent ones.

  7. I am a little confused as to why the eager fetch of post details on the one to one results in a subselect by default when hibernate could easily generate a joined statement like below.

    SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ , 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 p LEFT JOIN post_details pd ON pd.post_id = p.id
    WHERE p.id = 1

    Would this only happen if we specified the fetchType = JOIN on the relationship? I would be okay with hibernate eager loading the details relationship with one joined statement like above. I personally dislike the unidirectional relationship on a one-to-one.

  8. Where should I use the cascade.all, in the owning side or in the non-owning side?

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.