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

I'm running an online workshopk on the 14th of May about The Best Way to Fetch Data with Java Persistence and Hibernate.

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.

Transactions and Concurrency Control eBook

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

  1. Is it mandatory to provide the @Id in the PostDetails entity?

    @Id
    @GeneratedValue
    private Long id;
    

    Does it work without this @Id annotation?

    • An entity needs to have an identifier. Either you are using an @Id or an @EmbeddedId. @MapsId tells Hibernate to use the @Id column to fetch the parent post association.

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

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

  4. Hi Vlad,
    Thank you for your article.
    For your article to be seen by more people,I’d like to translate your article and post it on my blog if you do not mind.
    Of course, I will indicate the author and the original link in a conspicuous position.

    • You need to retain all marketing info, like links to book and courses and Hypersistence Optimizer. After you translate it, I need to take a look at it and tell you whether it’s fine or you need to change it.

  5. With @MapsId, does it still possible to get associated Detail from Post through getPostDetail() method?

  6. It better really be ‘the best way’, cause I implemented it and my salary depends on the incoming code review. Will update you.

  7. Hi, Vlad.

    Thank you for the explanation. I work with spring-data repositories and have a doubt regarding how to implement the repositories for the referenced entities.

    Where do you suggest to be put jpql queries of PostDetails entity, in the Post repository or in a specific repository for PostDetails?

    • If the PostDetails is always managed by the Post, then in the PostRepository, otherwise, on its own.

      • Thanks, Vlad.

        I would like your suggestion in the following case.

        Given that PostDetails will always be managed by Post and that the association is like you advised , unidirectional, and that I use spring-data-rest, so what do you think it is the best way for accessing the PostDetails object from the rest browser?

      • You can still fetch the PostDetails directly and modify it. As long as it shares the entity identifer with Post, it’s not difficult to navigate from its parent entity.

  8. Hi, considering the efficient way of mapping the OneToOne relationship above using MapsId, how do you delete PostDetails and automatically delete the corresponding Post?

    • As I explained in my book, High-Performance Java Persistence, you have many options. You can use DDL-level ON DELETE cascade, you can use JPA or HIbernate event listerens that autoatically remove the child as well, or you can simply issue the 2 remove operations.

  9. Lets say I want to remove a Post row. This wont affect the PostDetail corresponding that removed Post. However, I want that PostDetail to be removed as well. How to do that?

    • Of course, it will affect the PostDetails as the child cannot exist without its parent since the FK constraint will be violated. If you don’t map the association on the parent side, you have two options. Either you remove the Post and the PostDetails using their shared identifier or you delete only the Post and rely on FK-level delete cascading. Check out my High-Performance Java Persistence book for more details.

  10. Hello Vlad, thx for your great post.

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

    and Anthony Patricio 2009-08-11
    https://developer.jboss.org/wiki/SomeExplanationsOnLazyLoadingone-to-one
    states:

    So the resume: if your B->C mapping is mandatory (constrained=true), Hibernate will use proxy for C resulting in lazy initialization.

    Does this mean that above article is outdated and mistaken? Or I have misunderstood either you or Anthony?

    Regards,
    Jakub

    • Hibernate behavior keeps on changing, so it’s not unusual to get different results across versions. However, I doubt that the secondary statement will not be fired since there is no DB construct to ensure that a parent table row must always have an associated child row record. So, the best thing is to test it out. You can use my High-Performance Java Persistence GitHub repository for that. But, I’m 99.9% sure that it works as I said unless you enable bytecode enhancement which is a totally different story.

  11. Hi Vlad,

    Would it be possible to resolve the issue of lazy loading on the parent side of a one-to-one relationship through the use of a type similar to Optional?

    Obviously Optional itself could not be used because it is final and not serializable, but imagine a similar type MyOptional which did not have those problems.

    For example, if the field type in the parent entity were MyOptional, then with support from Hibernate, there’d be no need to check for the presence of the child entity — it could just return a “proxy” MyOptional which “lazy-checks” for the presence of the child entity.

      • Thanks for the quick reply & documentation.

        I know of the possibility of using bytecode enhancement, but of course that’s not a default configuration. I was mainly considering situations where it might not be possible or desirable to turn on bytecode enhancement. Do you think there is any value in such a feature in a future version of JPA/hibernate?

        Thanks, Shawn

      • I don’t think anyone will want to implement such a feature since bytecode enhancement already offers a solution for it. But since the JPA spec is open source, you should add an issue and see how it goes.

  12. [Solved] Hi Vlad, Thanks for the quick response. The High-Performance Java Persistence GitHub repository is truly an awesome & exhaustive collection of most possible JPA usecases. Very much helpful for budding JPA users like me, to be kept as reference. Thanks for creating it.

    For anyone having the same problem as me: Testcase ‘OneToOneMapsIdJoinColumnTest’ from above repository is closest to a Unidirectional-JoinColumn-MapsId-OnetoOne setup. And here lazy loading works!

    Comparing this to my environment, the repository test setup works on hibernate 5.4.2, while mine picked up a 5.3.9. Upgrading solved the problem with Lazy loading. Thanks !

  13. Hi Vlad, Great Article. Thanks for posting.
    I have come across the same problem as George. Hope you can help.

    Lazy loading at client side does stop working, when there is a @JoinColumn at client side.

    That is: If we query ‘PostDetails’, which has a @JoinColumn, mapping to any different column other than default, it eagerly fetches ‘Post’ also, with a second select query, regardless of the value of ‘fetch’ or ‘optional’ attribute.

    The ideal scenario of unidirectional one-to-one mapping using @JoinColumn & @MapsId at client side, and no mapping at Parent side, also has the same exact problem [https://vladmihalcea.com/change-one-to-one-primary-key-column-jpa-hibernate/]

    Is there something missing here. Thanks in advance.

    • Send me A Pull Request with a replicating test case in my High-Performance Java Persistence GitHub repository so that I can see the problem.

  14. I’ve a different scenario:

    A user may be also worker. A worker may not have a user.

    So I can’t use “userId” as PK of Worker because there may no User for some workers.
    I can’t use workerId as PK for User too because there may no Worker for that User.

    So in my schema: Worker will have a PK workerId and FK userId which is nullable and unique.

    How should I hibernate?

    • Then use a join table with unique constraints on each FK.

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.