The best way to write a custom Spring Data Repository

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 the best way to write a custom Spring Data Repository.

While the default JpaRepository methods, as well as the query methods, are very convenient in many situations, there might be times when you need custom Repository methods that can take advantage of any JPA provider-specific functionality.

When to use a custom Spring Data Repository

Let’s assume we want to fetch a one-to-many DTO projection, as I explained in this article.

Our JPA query looks like this:

List<PostDTO> postDTOs = entityManager.createQuery("""
    select p.id as p_id,
           p.title as p_title,
           pc.id as pc_id,
           pc.review as pc_review
    from PostComment pc
    join pc.post p
    order by pc.id
    """)
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(new PostDTOResultTransformer())
.getResultList();

Notice that we are unwrapping the JPA Query to a Hibernate org.hibernate.query.Query in order to provide a custom ResultTransformer that can build a hierarchical parent-child DTO aggregate from the default table-based Object[] projection.

We can’t just use a regular Spring Data Repository query method or a @Query annotation because we also have to pass our own Hibernate-specific ResultTransformer.

Therefore, we need to write a custom Repository that can provide us access to the underlying JPA EntityManager so that we can write our query using the Hibernate-specific API.

How to write a custom Spring Data Repository

First, we need to define an interface that provides the method signatures of our custom Repository methods.

public interface CustomPostRepository {

    List<PostDTO> findPostDTOWithComments();
}

Second, we need to provide an implementation of the CustomPostRepository interface:

public class CustomPostRepositoryImpl implements CustomPostRepository {
    
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<PostDTO> findPostDTOWithComments() {
        return entityManager.createNativeQuery("""
            SELECT p.id AS p_id, 
                   p.title AS p_title,
                   pc.id AS pc_id, 
                   pc.review AS pc_review
            FROM post p
            JOIN post_comment pc ON p.id = pc.post_id
            ORDER BY pc.id
            """)
        .unwrap(org.hibernate.query.Query.class)
        .setResultTransformer(new PostDTOResultTransformer())
        .getResultList();
    }
}

Third, we need to make the default Spring Data JPA PostRepository extend our CustomPostRepository interface:

@Repository
public interface PostRepository 
    extends JpaRepository<Post, Long>, CustomPostRepository {
}

A picture is worth 100 words, so here’s a diagram that shows you how the custom Spring Data Repository is associated to the standard JpaRepository one:

Custom Spring Data Repository class relationship

Testing time

Assuming we have two Post entities, the first one having two PostComment child entities, and the second Post having a single PostComment child:

entityManager.persist(
    new Post()
        .setId(1L)
        .setTitle("High-Performance Java Persistence")
        .addComment(
            new PostComment()
                .setId(1L)
                .setReview("Best book on JPA and Hibernate!")
        )
        .addComment(
            new PostComment()
                .setId(2L)
                .setReview("A must-read for every Java developer!")
        )
);


entityManager.persist(
    new Post()
        .setId(2L)
        .setTitle("Hypersistence Optimizer")
        .addComment(
            new PostComment()
                .setId(3L)
                .setReview("It's like pair programming with Vlad!")
        )
);

When calling the findPostDTOWithComments method, we are going to get the expected PostDTO hierarchical projection:

List<PostDTO> postDTOs = forumService.findPostDTOWithComments();

assertEquals(2, postDTOs.size());
assertEquals(2, postDTOs.get(0).getComments().size());
assertEquals(1, postDTOs.get(1).getComments().size());

PostDTO post1DTO = postDTOs.get(0);

assertEquals(1L, post1DTO.getId().longValue());
assertEquals(2, post1DTO.getComments().size());
assertEquals(1L, post1DTO.getComments().get(0).getId().longValue());
assertEquals(2L, post1DTO.getComments().get(1).getId().longValue());

PostDTO post2DTO = postDTOs.get(1);

assertEquals(2L, post2DTO.getId().longValue());
assertEquals(1, post2DTO.getComments().size());
assertEquals(3L, post2DTO.getComments().get(0).getId().longValue());

Cool, right?

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

Seize the deal! 40% discount. Seize the deal! 40% discount.

Conclusion

While it’s very common to see standard Spring Data repositories that extend the JpaRepository interface, a custom Repository can allow you to take advantage of all the features provided by JPA or the underlying JPA provider.

Transactions and Concurrency Control eBook

2 Comments on “The best way to write a custom Spring Data Repository

  1. Found this entry of @vladmihalcea because I was searching if default methods in the interface could be possible, @TomaszKucharzyk nice implementation you provide.

    @vladmihalcea What do you think about the use of default methods inside the @Repository custom Interface? In particular found this SO question https://stackoverflow.com/questions/64801649/spring-jpa-repository-interface-and-default-methods-use-case that shows this idea:

    @Repository
    public interface SomeRepository extends JpaRepository<SomeEntity, Integer> {
        default List listBySomeCriteria() {
            List result;
    
       //Some code
    
        return result;
    }
    

    I’m trying to use this idea using a CDI Application that integrates Spring Data JPA through its CDI Integration, in particular with multiple PUs in the same project as https://stackoverflow.com/questions/47527084/spring-data-jpa-cdi-integration-with-multiple-persistence-units @vladmihalcea Do you foresee any problem putting Spring Data JPA CDI Integration + Multiple PUs + @Repository with Default Methods together?

    Thank you!

    • Default methods will not help you since you will need access to the JPA EntityManager. Why would you try this method when a custom Repository is so much more flexible?

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.