How to emulate LEFT JOIN FETCH using Record-based projections

Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?

What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?

Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?

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

So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!

Introduction

In this article, I’m going to show you how to emulate a LEFT JOIN FETCH between two root entities without fetching the intermediate child entity.

I got the inspiration for this article by this comment thread on my blog.

Domain Model

Let’s assume we have the following entities:

Post with PostDetails and User

The Post is the root entity of our hierarchy, but just like it’s the case with the relational model, the root entity does not have any reference to its children:

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

On the second level of our hierarchy we have the PostDetails:

@Entity
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private LocalDateTime createdOn = LocalDateTime.now();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "created_by_user_id")
    private User createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;
}

The PostDetails entity has a @OneToOne association with Post and a @ManyToOne association with the User, which is mapped as follows:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    public String getFullName() {
        return String.join(" ", firstName, lastName);
    }
}

The Problem

We want to fetch a Post entity along with the associated User if there is any such association via the PostDetails entity.

If the @OneToOne association between the PostDetails and Post is mandatory, meaning that there is always a post_details record for a parent post record, we could fetch the PostDetails, Post, and User, like this:

PostDetails postDetails = entityManager.createQuery("""
    select pd
    from PostDetails pd
    join fetch pd.post p
    left join fetch pd.createdBy
    where pd.id = :postId
    """,
    PostDetails.class)
.setParameter("postId", 1L)
.getSingleResult();

Post post = postDetails.getPost();
assertEquals(
    "First post", 
    post.getTitle()
);
assertEquals(
    "John Doe", 
    postDetails.getCreatedBy().getFullName()
);

However, in our case, the PostDetails is optional, so we cannot use JOIN FETCH from the PostDetails side. And, even when there is a child record associated with a Post and aUser, we don’t want to fetch the PostDetails since we don’t need it for our business use case.

The Solution

To fetch the Post and the User, we could use the following JPQL query:

PostWithUserRecord postWithUserRecord = entityManager.createQuery("""
    select new PostWithUserRecord(p, u)
    from Post p
    left join PostDetails pd on pd.id = p.id
    left join pd.createdBy u
    where p.id = :postId
    """,
    PostWithUserRecord.class)
.setParameter("postId", 1L)
.getSingleResult();

assertEquals(
    "First post", 
    postWithUserRecord.post().getTitle()
);
assertEquals(
    "John Doe", 
    postWithUserRecord.user().getFullName()
);

The PostWithUserRecord is just a simple Java Record that’s meant to encapsulate both entities:

public record PostWithUserRecord(Post post, User user) {}

What’s interesting about this solution is that the Post and User entities are not simple projections. When Hibernate fetches the two entities, it will bind them to the current Persistence Context, so the entities will be managed, so if we modify them:

Post post = postWithUserRecord.post();
post.setTitle(post.getTitle() + " is awesome!");

SQLStatementCountValidator.reset();
entityManager.flush();
SQLStatementCountValidator.assertUpdateCount(1);

We can see that the UPDATE statement is generated during flush:

Query:["update post set title=? where id=?"], 
Params:[(First post is awesome!, 1)]

And, if we create a test case where we only have a Post with no PostDetails:

doInJPA(entityManager -> {
    entityManager.persist(
        new Post()
            .setId(1L)
            .setTitle("First post")
    );
});

When fetching the PostWithUserRecord, the User entity will be null and the fetched Post is a managed entity:

doInJPA(entityManager -> {
    PostWithUserRecord postWithUserRecord = entityManager.createQuery("""
        select new PostWithUserRecord(p, u)
        from Post p
        left join PostDetails pd on pd.id = p.id
        left join pd.createdBy u
        where p.id = :postId
        """,
            PostWithUserRecord.class)
    .setParameter("postId", 1L)
    .getSingleResult();

    assertEquals(
        "First post", 
        postWithUserRecord.post().getTitle()
    );
    assertNull(postWithUserRecord.user());

    Post post = postWithUserRecord.post();
    post.setTitle(post.getTitle() + " is awesome!");

    SQLStatementCountValidator.reset();
    entityManager.flush();
    SQLStatementCountValidator.assertUpdateCount(1);
});

Awesome, right?

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

Conclusion

The advantage of this solution is that you can use it with many unidirectional associations. Instead of mapping the @OneToMany side or the parent @OneToOne side, you can map only the child side that managed the Foreign Key column, and just use a query that fetches the parent and the child using a Record-based projection.

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.