3

How to use Java Records with Spring Data JPA

 6 months ago
source link: https://vladmihalcea.com/records-spring-data-jpa/
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

How to use Java Records with Spring Data JPA

Last modified:

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 we can use Java Records with Spring Data JPA Repositories.

As I already explained, Java Records cannot be used as JPA entities since the Records are immutable, and JPA requires the entity class to have a default constructor and be modifiable, as that’s how the entity properties are populated when the entity is being fetched from the database.

For this reason, this article will show you how to combine Java Records and JPA entities so that you get the best out of both.

Domain Mode

Let’s assume we have the following Post and PostComment entities:

Post and PostComment JPA entities

The Post is the parent entity in this one-to-many table relationship and is mapped like this:

@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
public Long getId() {
return id;
}
public Post setId(Long id) {
this.id = id;
return this;
}
public String getTitle() {
return title;
}
public Post setTitle(String title) {
this.title = title;
return this;
}
public List<PostComment> getComments() {
return comments;
}
public Post addComment(PostComment comment) {
comments.add(comment);
comment.setPost(this);
return this;
}
public PostRecord toRecord() {
return new PostRecord(
id,
title,
comments.stream().map(comment ->
new PostCommentRecord(
comment.getId(),
comment.getReview()
)
).toList()
);
}
}

The PostComment is the client entity in this relationship and is mapped as follows:

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {
@Id
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
private String review;
public Long getId() {
return id;
}
public PostComment setId(Long id) {
this.id = id;
return this;
}
public Post getPost() {
return post;
}
public PostComment setPost(Post post) {
this.post = post;
return this;
}
public String getReview() {
return review;
}
public PostComment setReview(String review) {
this.review = review;
return this;
}
}

However, since we don’t want to expose the JPA entities outside of the Service and Data Access Layer, we have created the following PostRecord and PostCommentRecord objects that we are going to share with the Web Layer:

PostRecord and PostCommentRecord

The PostRecord is created like this:

public record PostRecord(
Long id,
String title,
List<PostCommentRecord> comments
) {
public Post toPost() {
Post post = new Post()
.setId(id)
.setTitle(title);
comments.forEach(
comment -> post.addComment(comment.toPostComment())
);
return post;
}
}

An the PostCommentRecord is created as follows:

public record PostCommentRecord(
Long id,
String review
) {
public PostComment toPostComment() {
return new PostComment()
.setId(id)
.setReview(review);
}
}

As you can see in the JPA entity mappings and the Record classes, we have created several methods that allow us to create the Record from the JPA entity and vice versa. These methods will be used when exporting the Records from the service layer and importing the Records so that we can propagate the changes to the database via their associated JPA entities.

The Service and Data Access Layer

The Data Access Layer is very simple and consists of the following PostRepository:

@Repository
public interface PostRepository
extends BaseJpaRepository<Post, Long> {
@Query("""
select p
from Post p
join fetch p.comments
where p.id = :postId
""")
Optional<Post> findWithCommentsById(
@Param("postId") Long postId
);
}

The BaseJpaRepository is a replacement for the default Spring Data JpaRepository that removes dangerous methods, such as findAll or save, providing much better alternatives.

For more details about the BaseJpaRepository from Hypersistence Utils, check out this article.

The findWithCommentsById method allows us to fetch a Post entity along with all its associated PostComment child entities in a single SQL query.

The Service Layer provides the following ForumService Spring bean that the Web Layer will interact with:

@Service
@Transactional(readOnly = true)
public class ForumService {
@Autowired
private PostRepository postRepository;
public PostRecord findPostRecordById(Long postId) {
return postRepository
.findWithCommentsById(postId)
.map(Post::toRecord)
.orElse(null);
}
@Transactional
public Post insertPostRecord(PostRecord postRecord) {
return postRepository
.persist(postRecord.toPost());
}
@Transactional
public Post mergePostRecord(PostRecord postRecord) {
return postRepository
.merge(postRecord.toPost());
}
}

The findPostRecordById method fetches the Post entity and transforms it to a PostRecord using the toRecord method.

The insertPostRecord method takes a PostRecord object and persists the associated Post and PostComment child entities.

The mergePostRecord method takes a PostRecord object and merges the associated Post and PostComment child entities.

Testing Time

To test how the Java Records can be fetched, persisted, and merged using Spring Data JPA, we are going to call the aforementioned ForumService methods and see what statements are executed by Hibernate behind the scenes.

If we create a PostRecord object that has 5 associated PostCommentRecord child objects and pass the PostRecord to the insertPostRecord method:

forumService.insertPostRecord(
new PostRecord(
1L,
"High-Performance Java Persistence",
LongStream.rangeClosed(1, 5)
.mapToObj(i ->
new PostCommentRecord(
null,
String.format("Good review nr. %d", i)
)
)
.toList()
)
);

We can see that Spring Data JPA generates the following SQL INSERT statements:

Query:["
insert into post (title,id)
values (?,?)
"],
Params:[
(High-Performance Java Persistence, 1)
]
Query:["
insert into post_comment (post_id,review,id)
values (?,?,?)
"],
Params:[
(1, Good review nr. 1, 1),
(1, Good review nr. 2, 2),
(1, Good review nr. 3, 3),
(1, Good review nr. 4, 4),
(1, Good review nr. 5, 5)
]

Spring Data JPA can take advantage of the automatic JDBC batch insert feature provided by Hibernate if we configure the following settings:

properties.put(
AvailableSettings.STATEMENT_BATCH_SIZE,
50
);
properties.put(
AvailableSettings.ORDER_INSERTS,
Boolean.TRUE
);

For more details about the JDBC batch update mechanism, check out this article.

When fetching the PostRecord object using the findPostRecordById method:

PostRecord postRecord = forumService.findPostRecordById(1L);

Hibernate executes the following SQL SELECT query:

Query:["
select
p1_0.id,
c1_0.post_id,
c1_0.id,
c1_0.review,
p1_0.title
from
post p1_0
join post_comment c1_0 on p1_0.id=c1_0.post_id
where
p1_0.id=?
"],
Params:[(1)]

Now, we could send the PostRecord as a JSON object to the front-end layer as it’s very easy to convert Java Records to JSON using Jackson:

For instance, if we log the JSON representation of the PostRecord we’ve just fetched:

LOGGER.info("PostRecord to JSON: {}", JacksonUtil.toString(postRecord));

We are going to see the following JSON object printed to the log:

{
"id": 1,
"title": "High-Performance Java Persistence",
"comments": [
{
"id": 1,
"review": "Good review nr. 1"
},
{
"id": 2,
"review": "Good review nr. 2"
},
{
"id": 3,
"review": "Good review nr. 3"
},
{
"id": 4,
"review": "Good review nr. 4"
},
{
"id": 5,
"review": "Good review nr. 5"
}
]
}

If the front-end modifies the Post and PostComment objects and sends is the following JSON object that we pass to the mergePostRecord method:

String upatedPostRecordJSONSTring = """
{
"id": 1,
"title": "High-Performance Java Persistence, 2nd edition",
"comments": [
{
"id": 1,
"review": "Best book on JPA and Hibernate!"
},
{
"id": 2,
"review": "A must-read for every Java developer!"
}
]
}
""";
forumService.mergePostRecord(
JacksonUtil.fromString(
upatedPostRecordJSONSTring,
PostRecord.class
)
);

Then we can see that Spring Data JPA will execute the following SQL statements:

Query:["
select
p1_0.id,
p1_0.title,
c1_0.post_id,
c1_0.id,
c1_0.review
from
post p1_0
left join post_comment c1_0 on p1_0.id=c1_0.post_id
where p1_0.id=?
"],
Params:[(1)]
Query:["
update post
set title=?
where id=?
"],
Params:[(High-Performance Java Persistence, 2nd edition, 1)]
Query:["
update post_comment
set post_id=?,review=?
where id=?
"],
Params:[
(1, Best book on JPA and Hibernate!, 1),
(1, A must-read for every Java developer!, 2)
]
Query:["
delete from post_comment
where id=?
"],
Params:[
(3),
(4),
(5)
]

The first SQL SELECT query is executed by merge in order to fetch the latest state of the Post and PostComment entities so that the Hibernate dirty checking mechanism can determine what has changed.

For more details about how the persist and merge JPA operations work, check out this article.

The UPDATE statements that follow are for the post and post_comment table records that have changed, and the DELETE statements are for the records that the client has removed.

I'm running a High-Performance Spring Data JPA Online Workshop on the 12th of March.

WorkshopHPSDJPAOnline_h200.jpg

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

HPJP_h200.jpg

HPJP_Video_Vertical_h200.jpg

HPSQL_Video_Course_h200.jpg

Conclusion

Java Records can be very useful when using Spring Data JPA because they allow us to isolate the JPA entities to the Service and Data Access Layer.

Transforming the JPA entities to Java Records and from Java Records to JPA entities is rather straightforward, and this way, we don’t have to worry about N+1 query issues or LazyInitializationException problems as Java Records have all the data already pre-fetched.

For more details about Java Records, check out this article as well.

Transactions and Concurrency Control eBook

4 Comments on “How to use Java Records with Spring Data JPA”

  1. For the insert use case, how would we know what the ID value should be? In your example you put 1L but can we expect the caller to know what the next ID should be?

    • If you take a look at the PostComment, you’ll see that the id is null, as it’s auto-generated.

      I could have done the same thing for Post, but for the sake of readability, I deliberately made the Post use an assigned generator so that you can see the same id when I insert, select and update it.

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