2

The Spring Data JPA findById Anti-Pattern

 1 year ago
source link: https://vladmihalcea.com/spring-data-jpa-findbyid/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

The Spring Data JPA findById Anti-Pattern

Last modified: Nov 15, 2022

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 how the Spring Data JPA findById method can become an Anti-Pattern when using it to reference parent entity associations.

Domain Model

Let’s consider we have a PostComment child entity that is associated with the parent Post entity via the post reference in the PostComment entity:

Post and PostComment entities

The Post entity is mapped as follows:

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

And, the PostComment entity maps the Foreign Key column using the @ManyToOne annotation:

@Entity
@Table(name = "post_comment")
public class PostComment {
@Id
@GeneratedValue
private Long id;
private String review;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
}

Spring Data JPA findById Anti-Pattern

Let’s assume we want to provide an addNewPostComment method with the following signature:

PostComment addNewPostComment(String review, Long postId);

The most typical way the addNewPostComment method is usually implemented is this:

@Transactional(readOnly = true)
public class PostServiceImpl implements PostService {
@Autowired
private PostRepository postRepository;
@Autowired
private PostCommentRepository postCommentRepository;
@Transactional
public PostComment addNewPostComment(String review, Long postId) {           
PostComment comment = new PostComment()
.setReview(review)
.setPost(
postRepository.findById(postId).orElse(null)
);
postCommentRepository.save(comment);
return comment;
}
}

However, when calling the addNewPostComment method:

postService.addNewPostComment(
"Best book on JPA and Hibernate!",
postId
);

We’ll see that Spring Data JPA generates the following SQL statements:

SELECT
post0_.id AS id1_0_0_,
post0_.slug AS slug2_0_0_,
post0_.title AS title3_0_0_
FROM
post post0_
WHERE
post0_.id = 1
SELECT nextval ('hibernate_sequence')
INSERT INTO post_comment (
post_id,
review,
id
)
VALUES (
1,
'Best book on JPA and Hibernate!',
1
)

Every time I run this example during my High-Performance Java Persistence training, I ask my students where does the first SQL query come from:

SELECT
post0_.id AS id1_0_0_,
post0_.slug AS slug2_0_0_,
post0_.title AS title3_0_0_
FROM
post post0_
WHERE
post0_.id = 1

This query was generated by the findById method call, which is meant to load the entity in the current Persistence Context. However, in our case, we don’t need that. We just want to save a new PostComment entity and set the post_id Foreign Key column to a value that we already know.

But, since the only way to set the underlying post_id column value is to provide a Post entity reference, that’s why many developers end up calling the findById method.

In our case, running this SQL query is unnecessary because we don’t need to fetch the parent Post entity. But how can we get rid of this extra SQL query?

How to fix the Spring Data JPA findById Anti-Pattern

The fix is actually very easy. Instead of using findById, we need to use the getReferenceById method that’s inherited automatically from the JpaRepository:

@Transactional
public PostComment addNewPostComment(String review, Long postId) {
PostComment comment = new PostComment()
.setReview(review)
.setPost(postRepository.getReferenceById(postId));
postCommentRepository.save(comment);
return comment;
}

That’s it!

When calling the same addNewPostComment method now, we see that the Post entity is no longer fetched:

SELECT nextval ('hibernate_sequence')
INSERT INTO post_comment (
post_id,
review,
id
)
VALUES (
1,
'Best book on JPA and Hibernate!',
1
)

The reason why the Post entity is no longer fetched is that the getReferenceById method calls the getReference method of the underlying EntityManager, giving you an entity Proxy instead:

public T getReferenceById(ID id) {
Assert.notNull(id, "The given id must not be null!");
return this.em.getReference(this.getDomainClass(), id);
}

An entity Proxy is sufficient for our use case because HIbernate will only call the getId method on it in order to set the underlying post_id column value before executing the SQL INSERT statement.

And since the Proxy already has the id value as we provided it to the getReferenceById method, there is no need for Hibernate to initialize it with a secondary SQL query as long as only the getId method is called on the Proxy entity.

On the other hand, if we called any other method on the Proxy object, Hibernate would have to trigger the Proxy initialization, and an SQL SELECT statement would be expected to load the entity object from the database.

For more details about the difference between the find and the getReference methods of the JPA EntityManager, check out this article as well.

Third time’s a charm!

Now, how many times have you seen anyone use the getReferenceById method in any Spring Data JPA project you’ve ever worked on?

If you haven’t seen it very often, it’s understandable. Naming things is hard.

There are only two hard things in Computer Science: cache invalidation and naming things.

— Phil Karlton.

Initially, Spring Data JPA offered a getOne method that we should call in order to get an entity Proxy. But we can all agree that getOne is not very intuitive.

So, in the 2.5 version, the getOne method got deprecated in favor of the getById method alternative, that’s just as unintuitive as its previous version.

Neither getOne nor getById is self-explanatory. Without reading the underlying Spring source code or the underlying Javadoc, would you know that these are the Spring Data JPA methods to call when you need to get an entity Proxy?

Therefore, in the 2.7 version, the getById method was also deprecated, and now we have the getReferenceById method instead, as illustrated by the Spring Data SimpleJpaRepository implementation:

@Deprecated
public T getOne(ID id) {
return this.getReferenceById(id);
}
@Deprecated
public T getById(ID id) {
return this.getReferenceById(id);
}

In my opinion, even the JPA getReference method name was badly chosen. I’d have called it getProxy, as that’s exactly what it returns, an entity proxy.

If the JPA method were called getProxy and the JpaRepository offered a getProxyById method, I think we’d have seen this method being used much more often.

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

Conclusion

While the findById method is very useful when you really need to fetch an entity, to create a child entity, you don’t need to fetch the parent entity just to set the Foreign Key column value.

Especially for batch processing tasks that need to create many such entities, the findById Anti-Pattern can easily lead to N+1 query issues, so better avoid it.

Transactions and Concurrency Control eBook

2 Comments on “The Spring Data JPA findById Anti-Pattern”

  1. Perfect, that was what I was looking for. I knew that something similar could be achieved with EntityManager but this is even better, only using the repository.

    Keep up with your work, really useful!

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Comment *

Before posting the comment, please take the time to read the FAQ page

Name *

Email *

Website

Notify me of follow-up comments by email.

This site uses Akismet to reduce spam. Learn how your comment data is processed.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK