How to update only a subset of entity attributes using JPA and Hibernate @DynamicUpdate
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
One of my Twitter followers asked me to answer the following question on StackOverflow.
This article aims to explain various techniques for controlling the UPDATE
SQL statement that is executed whenever an entity is modified by the data access layer.
Domain Model
Let’s assume we have the following Post
entity:
@Entity(name = "Post") @Table(name = "post") public class Post { @Id private Long id; private String title; private long score; @Column( name = "created_on", nullable = false, updatable = false ) private Timestamp createdOn; @Transient private String creationTimestamp; public Post() { this.createdOn = new Timestamp( System.currentTimeMillis() ); } public String getCreationTimestamp() { if(creationTimestamp == null) { creationTimestamp = DateTimeFormatter .ISO_DATE_TIME.format( createdOn.toLocalDateTime() ); } return creationTimestamp; } @Override public String toString() { return String.format( "Post{\n" + " id=%d\n" + " title='%s'\n" + " score=%d\n" + " creationTimestamp='%s'\n" + '}', id, title, score, getCreationTimestamp() ); } //Getters and setters omitted for brevity }
Insert-only columns
The Post
entity has a createdOn
attribute which should only be set when the entity is created for the first time. Any successive UPDATE
is not allowed to modify this database column, therefore the updatable
attribute of the associated @Column
annotation is set to false
.
@Column( name = "created_on", nullable = false, updatable = false ) private Timestamp createdOn;
For entity attribute that should never modify after the entity is persisted (insert-only columns), you should use
@Column(updatable = false)
.
Calculated entity attributes
The Post
entity createdOn
attribute is a Timestamp
which we might want to print in application logs using the ISO_DATE_TIME
DateTimeFormatter
. To avoid calculating the String
object representation every time we need to log this entity, the creationTimestamp
attribute is going to store this value.
@Transient private String creationTimestamp;
However, we don’t want this attribute to be persisted. Hence, we need to annotate it with the @Transient
. This way, Hibernate is going to ignore this attribute when translating the entity state modifications into an SQL statement.
Assuming we have persisted the following entity:
doInJPA(entityManager -> { Post post = new Post(); post.setId(1L); post.setTitle("High-Performance Java Persistence"); entityManager.persist(post); });
When fetching, logging, and modifying this entity:
doInJPA(entityManager -> { Post post = entityManager.find(Post.class, 1L); LOGGER.info("Fetched post: {}", post); post.setScore(12); });
The following output is obtained:
SELECT p.id AS id1_0_0_, p.created_on AS created_2_0_0_, p.score AS score3_0_0_, p.title AS title4_0_0_ FROM post p WHERE p.id = 1 -- Fetched post: Post{ id=1 title='High-Performance Java Persistence' score=0 creationTimestamp='2016-10-10T16:48:25.566' } UPDATE post SET score = 12, title = 'High-Performance Java Persistence' WHERE id = 1
As you can see, neither the createdOn
not the creationTimestamp
are included in the UPDATE
SQL statement.
For calculated attributes that should never be persisted in the associated database table, you should use
@Transient
.
Dynamic updates
The previous UPDATE
statement includes all table columns, even if only a subset is being actually modified. Using the same SQL statement is beneficial when using JDBC statement caching. However, if the database table is heavily indexed, we don’t want to update certain index entries that have not been modified, as explained by Markus Winand.
For this reason, Hibernate offers the @DynamicUpdate
annotation. All we need to do is to add this annotation at the entity level:
@Entity(name = "Post") @Table(name = "post") @DynamicUpdate public class Post { //Code omitted for brevity }
Now, when executing the previous test case which modified the score
attribute, the following UPDATE
statement is executed:
UPDATE post SET score = 12, WHERE id = 1
Brilliant!
To update only the columns that got modified, the entity must use the
@DynamicUpdate
annotation, which is also mandatory when using versionless optimistic locking as well.
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
Flexibility is very important when dealing with a data access framework, and, for this reason, Hibernate offers several options to avoid updating certain columns, or to update just the columns that got modified.
