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.

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

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.

Transactions and Concurrency Control eBook

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.