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.

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 ManyToOne association maps to a one-to-many table relationship

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.

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

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.