The Spring Data JPA findById Anti-Pattern
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.
The Spring Data JPA findById Anti-Pattern
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:
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 thegetReference
methods of the JPAEntityManager
, 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.
2 Comments on “The Spring Data JPA findById Anti-Pattern”
-
Joellao
November 15, 2022Perfect, 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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK