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:
- To avoid loading large columns (e.g., JSON)
- To avoid N+1 query issues for bidirectional
@OneToOne
associations
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
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.