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

(Last Updated On: April 9, 2019)
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.

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.

Download free ebook sample

Newsletter logo
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.

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

  1. Hello Vlad
    I’m having a problem with the previous code, except I’m using JpaRepositories from spring-data-jpa rather than entitymanager itself.

    The code get the error:
    org.springframework.orm.jpa.JpaSystemException: null id generated for:class xxy.test.jpa.onetoonetest.TestEntityOneToOneExample$PostDetails; nested exception is org.hibernate.id.IdentifierGenerationException: null id generated for:class xxy.test.jpa.onetoonetest.TestEntityOneToOneExample$PostDetails

    And my solution to the previous error is to delete the “optional = false” in the OnetoOne annotation on Post.PostDetails. In this way the save method works but I don’t quite understand. Do you have any explanations for this?

    Thanks!

    • Maybe it’s a Spring issue due to their save method hacks. Or, it could be a Hibernate issue. You need to investigate it and see what’s the real cause.

  2. Hey Vlad!

    As always, wonderful post we got here.
    I have a question on this approach of OneToOne relationship.

    Imagina a situation where I have a Doctor and a Doctor have one (and only one) Address.
    Address and Doctor are different entities and resides on separate tables.
    Until here, everything is OK.

    Now, when I try to persist a new Doctor with a new Address attached to it, Hibernate throw an

    org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property
    
  3. Hi Vlad,

    In your last example with @MapsId, yes there is no cascading defined. Then when deleting the main Post entity will PostDetails be deleted automatically? If not, I see this as the only downside that often it is very convenient just to delete the main entity assuming that child entities will be deleted on cascade.

  4. Would a set up like this work with a database schema that is already set?
    For Ex: I have a db schema that contains the tables post and post_detail. post_detail has the fk constraint for post_id defined in the ddl. In PostDetail class the private Long id; is switched to the name postId and the @Column annotation like so, @Column(name=”post_id”) would the @MapsId annotation over the private Post post; map to the right id? Or does the @MapsId attempt to create another column? I am currently trying to implement this type of OneToOne relationship with a DB schema I have already implemented. I am getting an error during schema-validation that my child table is missing an id column to the parent table that I have not defined.

    • If you have a separate FK column, then you don’t have a test one-to-one table relationship. You have a one-to-many relationship with a unique constraint. Without changing the schema, you can’t use MapsId. You need to change The schema if you want to be more efficient.

      • I am a little confused. In the example provided @MapsId makes it so post_detail’s id is the same id as post (from my understanding). In my example, the database already exists. In this database, post_detail has a post_id column that is both the fk and primary key in post_detail which is the primary key from the post table (from my understanding this is what @MapsId does). Is that not the same?

        If that’s not the case, is that because post_detail has no actual id column in the db, and the @MapsId annotation links them together under the hood some how?

      • If you are sharing the PK bug want to customize the column name, you need to use the `@JoinColumn(name=”id”) next to MapsId.

  5. What if I wanted do persist Post with its PostDetails at the same time using cascade, when Post does not have its Id yet ? I keep getting error ;Caused by: org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property; which is quite logical for me – the question is is there any different way of persisting @OneToOne with @MapsId, since this example shows only the solution where Post is already persisted in the db ?

    • As long as you set both sides of the association, the cascading should work. If it does not work, you have to write a replicating test case and open a Jira issue for the Hibernate project.

  6. Hey Vlad, thought this was great article it really helped me understand whats happening under the hood in JPA.

    I was wondering on your final example what the Post class looks like.
    I think you mentioned it was undirectional so I assume it doesn’t hold a reference to its child(PostDetails) anymore like the previous examples. Is that correct?

    Also is it only possible to persist from the child in your final example since its the only one that has the logic to tell about the relationship? If that is true, is that bad practice to try to persist from the parent? I am asking because I am attempting to figure out a way to persist the parent and have it set up its relationship to its child.

    Thanks!

    • In my last example, you don’t need to map the PostDetails in the Post entity. Also, you don’t need to cascade either. Just create the two entities and call persist for both. It’s just one extra line of code.

      • I see what you mean in for this example. What I am trying do is ingest a json file into the database, but it is nested pretty deep at certain points so it makes logic in the main a little more tedious calling persist on every nested child and its parent. Although I believe it probably is doable. Would calling persist on the root parent and have it recursively setup those relationships be extremely performant costly, is that why you prefer this method?

      • The problem comes when you need to fetch the entity. You can map two Post entities to the post table, one with the details for cascading while the other without the details. Check out this article for more details.

  7. Hi,

    I’m used to set a post_details_id in Post so I can put a not null constraint (each Post needs one PostDetails). How can I add the constraint with your example? Now I can add a Post without PostDetails in the database.

    • The Post is the parent, so it should not contain a FK. Also, a true one-to-one association shares the PK, so you don’t need a separate FK column. As for adding constraints, that should be done with FlywayDB, not Hibernate mappings.

      • Ok, but how to say “every Post needs to have one PostDetails”? Cause now you can have a Post without a PostDetails in the database. Seems like you can’t do this with this mapping…?

      • If every Post should have a PostDetails, then why use 2 tables? It looks like a single table us what you need.

  8. I have two entitites one of which is mapped to the other by @MapsId. Now, If I do findAll() for post, I want to be able to fetch the associated oneToOne postDetails enities also. If I do findAll(), it fires db query to fetch all rows of post and then one by one it fetches postDetails using one sql query at a time. Is there a way to fetch all the data in a single sql query.

  9. Hi Vlad, What a great explanation there. I like your articles very much.

    Btw I have a problem with this @MapsId and setDetails() method.

    We knew that @MapsId made PostDetails use Post id as its id. How if we want to change the details (child) with another object? because when i use that using some test case like below, it thrown a DataIntegrityViolationException

    public void testCannotChangeDetails_ThrownException(){
           // post is persisted in @Before method
            post.setDetails(new PostDetails());
            postRepository.save(post);
    
            post.setDetails(new PostDetails());
            postRepository.save(post);
    }
    

    I understood why it’s thrown that Exception, but logically is, it allowed to change the child right? Or we need to delete the child first?

    thank you for your help.

    • The Child shared the same ID with the Parent entity. So, if you could change the Child, it means you would have to change the Parent table PK, which is not allowed. In your case, you probably need a @ManyToOne association with a unique constraint on the FK side.

      • Actually, I just use your code in this article.

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

        So I need @OneToOne instead.

        from your answer, I think it is conform that when we use @MapsId we will never be allowed to change the child, except we delete it first and create a new child and then add it to the parent.

        Is my confirmation above is correct?

      • The setDetails method is generic for both use cases: with or without @MapsId. If you are using @MapsId, you cannot change the Child record. If you want to do that, you need a separate FK, meaning that you need to remove the @MapsId annotation.

  10. hi vlad,
    can we have lazy fetch on primary key @Id , autogenerate . What will be the implications incase

    • I don’t understand your question. Lazy is useful for associations not numerical properties

  11. Hi Vlad,

    when we tried to do this with a unidirectional relationship, we found that “entityManager.persist(details);” threw an exception because it tried to insert the Post entity into the database again, which created a primary key conflict.

    Probably this is because the Post entity was detached.

    Our workaround is to set the id field explicitly when setting the Post entity:
    setPost(Post post){
    this.post=post;
    this.id=post.getId();
    }
    This is somewhat weird.

    Is it possible to explicitly define the join column name of the post_id column in the details table via @JoinColumn? For us, Hibernate throws org.hibernate.id.IdentifierGenerationException: attempted to assign id from null one-to-one property

    • You can find some examples for both your use cases in the GitHub repository and work like a charm.

      • You wrote about GitHub, but when I went to your repo, Post and PostDetails were using annotations from the example you told that is not optimal. There was no MapsId.

      • If you check the test with the MapsId in its title, you’ll find it. There are more than 300 tests there.

  12. Does it make any difference if you put @MapsId in PostDetails or in Post? Should I add @MapsId to both sides of a bidirectional association?

    • No. You should not add MapsId on both sides. Only on the FK side.

  13. Hi. Thank you very much for you post.
    I have a problem.
    How can I do a select query on Post without select on PostDetails?
    I am using spring jpa and in PostRepository I have :

    @Query(value = “select * from post p where p.title=:title”, nativeQuery = true)
    Post findPostByTitle(@Param(“title”) String title);

    this query result in two select query by hibernate :

    Hibernate: select * from post p where p.title=?
    Hibernate: select postdetails0 … where postdetails0_.post_id=?

    Is there any way to reduce to one query (on Post) while use @MapsId method?

  14. Hi Vlad,

    The lazy loading does work if we don’t have a join column as is explained in the article. But as we add a @JoinColumn as may be needed to use a different primary key column name(other than user_id format) the lazy loading doesn’t seem to work.

    We have 2 queries for each select. Is that expected behavior?

    Thanks,
    George

    • For client side, lazy loading will always work with or without @JoinColumn.

  15. Hey Vlad,

    I am struggling with a technical limitation and I can’t find the solution anywhere…
    Basically, taking your DB structure, I would like to:
    1. Insert Post_1 + PostDetails_1
    2. Insert Post_2 and make PostDetails_1 point to Post_2, unlinking it from Post_1.

    I want to do each step in it’s separate transaction (so 2 transactions in total).

    I tried:
    – Manually assigning the ID –> ofc it doesn’t work, can’t manually alter the ID
    – Cloning the PostDetails_1 and inserting the new clone while deleting the original object.

    Could you please help me?

    Thanks!
    Matt

    • If the PK is shared, you can reassign a child entity to a new parent.

      If you don’t share the PK, meaning you have both an @Id and a @OneToOne association using a separate FK, then you can do it.

      • The PK is the same as the FK.
        The @Id of the PostDetails is the PK and FK.

        Both of the things I’ve tried failed.
        – Manually reassigning fails
        – Deleting and inserting fails as well with attempted to assign id from null one to one.
        I have set the relationship before saving postdetails_1.setPost(post_2)

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.