ManyToOne JPA and Hibernate association best practices
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, I’m going to show you what is the best way to map a ManyToOne association when using JPA and Hibernate.
Since the @ManyToOne
association is the most common relationship, knowing how to map it properly will have a significant impact on application performance.
A one-to-many is the most common relationship in database tables@vlad_mihalcea explains the best way to map ManyToOne associations when using JPA and Hibernatehttps://t.co/M1U9fwdTdo pic.twitter.com/sQ4yt8aMQ7
— SQL Daily (@sqldaily) May 14, 2020
Table relationships
As explained in this article, there are three table relationship types:
- one-to-many
- one-to-one
- many-to-many
The one-to-many table relationship looks as follows:
The post_comment
table has a post_id
column that has a Foreign Key relationship with the id
column in the parent post
table. The post_id
Foreign Key column drives the one-to-many table relationship.
The @ManyToOne JPA and Hibernate association
When using JPA and Hibernate, the @ManyToOne
annotation allows you to map a Foreign Key column:
@Entity(name = "PostComment") @Table(name = "post_comment") public class PostComment { @Id @GeneratedValue private Long id; private String review; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; //Getters and setters omitted for brevity }
The @JoinColumn
annotation allows you to specify the Foreign Key column name. In our example, we can omit the @JoinColumn
annotation since, by default, the Foreign Key column name is assumed to be formed by joining the @ManyToOne
property and the parent entity identifier via the _
character.
Also, it’s very important to set the fetch strategy explicitly to FetchType.LAZY
. By default, @ManyToOne
associations use the FetchType.EAGER
strategy, which can lead to N+1 query issues or fetching more data than necessary.
For more details about why you should avoid using
FetchType.EAGER
, check out this article.
Persisting a ManyToOne association with JPA and Hibernate
Let’s assume we have previously persisted a parent Post
entity:
entityManager.persist( new Post() .setId(1L) .setTitle("High-Performance Java Persistence") );
A common mistake developers do when persisting child entities is to fetch the parent entity using find
:
Post post = entityManager.find(Post.class, 1L); entityManager.persist( new PostComment() .setId(1L) .setReview("Amazing book!") .setPost(post) );
Or, if you’re using Spring Data JPA, the same issue happens when using the findById
method of the JpaRepository
:
Post post = postRepository.findById(1L); commentRepository.save( new PostComment() .setId(1L) .setReview("Amazing book!") .setPost(post) );
When persisting the PostComment
entity while using the find
method, Hibernate will execute the following SQL statements:
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id=1 INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'Amazing book!', 1 )
The SELECT query is not needed since we are not interested in fetching the Post
entity. All we want is to set the post_id
Foreign Key column.
So, instead of find
, you need to use getReference
:
Post post = entityManager.getReference(Post.class, 1L); entityManager.persist( new PostComment() .setId(1L) .setReview("Amazing book!") .setPost(post) );
Or the getReferenceById
method if you’re using Spring Data JPA:
Post post = postRepository.getReferenceById(1L); commentRepository.save( new PostComment() .setId(1L) .setReview("Amazing book!") .setPost(post) );
It’s unfortunate that the JpaRepository
method is called getReferenceById
and not getProxyById
, as it would be much easier for developers to guess its purpose.
For more details about the
findById
Spring Data JPA Anti-Pattern, check out this article.
Now, Hibernate doesn’t need to execute the SELECT statement:
INSERT INTO post_comment ( post_id, review, id ) VALUES ( 1, 'Amazing book!', 1 )
YouTube Video
I also published a YouTube video about the @ManyToOne
association, so enjoy watching it if you’re interested in this topic.
Fetching a ManyToOne association with JPA and Hibernate
Assuming you are using the FetchType.LAZY
strategy, when fetching the PostComment
entity and accessing the post
@ManyToOne
association:
PostComment comment = entityManager.find(PostComment.class, 1L); LOGGER.info( "The post '{}' got the following comment '{}'", comment.getPost().getTitle(), comment.getReview() );
Hibernate is going to trigger a secondary SELECT statement:
SELECT pc.id AS id1_1_0_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_ FROM post_comment pc WHERE pc.id = 1 SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1 The post 'High-Performance Java Persistence' got the following comment 'Amazing book!'
To avoid the secondary SELECT query, you need to fetch the post
@ManyToOne
association using the JOIN FETCH
directive:
PostComment comment = entityManager.createQuery(""" select pc from PostComment pc join fetch pc.post where pc.id = :id """, PostComment.class) .setParameter("id", 1L) .getSingleResult(); LOGGER.info( "The post '{}' got the following comment '{}'", comment.getPost().getTitle(), comment.getReview() );
Now, Hibernate executes a single SQL query to fetch both the child and parent entities:
SELECT pc.id AS id1_1_0_, p.id AS id1_0_1_, pc.post_id AS post_id3_1_0_, pc.review AS review2_1_0_, p.title AS title2_0_1_ FROM post_comment pc INNER JOIN post p ON pc.post_id = p.id WHERE pc.id = 1 The post 'High-Performance Java Persistence' got the following comment 'Amazing book!'
The JOIN FETCH
directive can help you avoid getting a LazyInitializationException
if you try to access a lazy @ManyToOne
association after the Persistence Context is closed.
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
When using JPA and Hibernate, it’s very important to know how to map and use the ManyToOne association since it’s the most common relationship.
Using FetchType.LAZY
, by default, is a very useful practice, as the fetching strategy should be set on a per-use case basis, not globally.
