The best way to map multiple entities on the same table

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, we are going to see what is the best way to map multiple entities on the same table.

There are several advantages to mapping multiple entities on the same database table:

Before we start investigating the best way to map multiple entities on the same table, if you wonder why you even need to use one-to-one table relationships, then check out this article first.

Domain Model

Let’s assume we are using the following JPA entities:

The Post, PostDetails, PostSummary entities

The Post entity is mapped like this:

@Entity
@Table(name = "post")
public class Post {
⠀
    @Id
    private Long id;
⠀
    private String title;
⠀
    @OneToOne(
        mappedBy = "post", 
        cascade = CascadeType.ALL, 
        fetch = FetchType.LAZY
    )
    private PostDetails details;
}

The PostDetails entity uses the @MapsId annotation to map the one-to-one table relationship by sharing the Primary Key with the parent post table:

@Entity
@Table(name = "post_details")
public class PostDetails {
⠀
    @Id
    private Long id;
⠀
    @Column(name = "created_on")
    private LocalDateTime createdOn = LocalDateTime.now();
⠀
    @Column(name = "created_by")
    private String createdBy;
⠀
    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name = "id")
    private Post post;
}

Fetching the parent entity

The problem with the bidirectional @OneToOne association is that when you fetch the parent entity:

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

The child entity is fetched using a secondary query in spite of the FetchType.LAZY strategy:

SELECT 
    p.id,
    p.title 
FROM 
    post p 
WHERE 
    p.id = 1

SELECT 
    pd.id,
    pd.created_by,
    pd.created_on 
FROM 
    post_details pd 
WHERE 
    pd.id = 1

ERROR [main]: Hypersistence Optimizer - CRITICAL - SecondaryQueryEntityFetchingEvent - 
The [io.hypersistence.optimizer.hibernate.session.merge.jpa.extra.PostDetails] entity 
with the identifier value of [1] has been loaded using a secondary query 
instead of being fetched using a JOIN FETCH clause.

The SecondaryQueryEntityFetchingEvent is reported by Hypersistence Optimizer because fetching the parent entity triggered a secondary query to fetch the child entity as well.

Mapping an additional entity on the post table

To avoid the secondary query that is triggered by fetching the parent entity, we have two options:

For this article, we are going to choose the second option and map the following PostSummary entity:

@Entity
@Table(name = "post")
public class PostSummary {
⠀
    @Id
    private Long id;
⠀
    private String title;
}

Because we avoid mapping the details association, when fetching the PostSummary entity:

PostSummary postSummary = entityManager.find(PostSummary.class, 1L);
⠀
assertNoEventTriggered(SecondaryQueryEntityFetchingEvent.class);

A single SQL query is generated this time:

SELECT 
    p.id,
    p.title 
FROM 
    post p 
WHERE 
    p.id = 1

However, now we risk fetching both the Post and the PostSummary in the same Persistence Context and end in a Last Writer Wins situation:

Post post = entityManager.find(Post.class, 1L);
PostSummary postSummary = entityManager.find(PostSummary.class, 1L);
⠀
post.setTitle("High-Performance Java Persistence, 2nd edition");
postSummary.setTitle("High-Performance Java Persistence, second edition");
⠀
assertEventTriggered(1, TableRowAlreadyManagedEvent.class);

Luckily, if Hypersistence Optimizer is enabled, a TableRowAlreadyManagedEvent will be triggered to notify you about this issue:

ERROR [main]: Hypersistence Optimizer - CRITICAL - TableRowAlreadyManagedEvent - 
The table row associated with the 
[io.hypersistence.optimizer.hibernate.session.merge.jpa.extra.PostSummary] entity 
with the identifier value of [1] is already managed by the 
[io.hypersistence.optimizer.hibernate.session.merge.jpa.extra.Post] entity 
in the current Hibernate Session.

UPDATE 
    post 
SET 
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE 
    id = 1

UPDATE 
    post 
SET 
    title = 'High-Performance Java Persistence, second edition'
WHERE 
    id = 1

So, if you are using JPA and Hibernate, Hypersistence Optimizer will surely help you get the best out of your data access layer.

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

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

Conclusion

When mapping a one-to-one table relationship, it’s better to avoid mapping the association on the parent side.

However, if you inherit a legacy application and cannot just drop the parent-side association, then you can use a secondary entity that doesn’t map the child entity association. Nevertheless, you will have to make sure that you don’t end up fetching both entities that map the same table record.

Transactions and Concurrency Control eBook

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.