How to map calculated properties with Hibernate @Generated annotation

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

As I explained in this previous article, you can map calculated properties using Hibernate @Formula, and the value is generated at query time.

In this post, you’ll see how you can calculate an entity property at INSERT or UPDATE time.

Domain Model

Assuming we have the following Hero entity mapping:

@Entity(name = "Hero")
public class Hero {

    @Id
    private Long id;

    private String firstName;

    private String lastName;

    private String middleName1;

    private String middleName2;

    private String middleName3;

    private String middleName4;

    private String middleName5;

    @Generated( value = GenerationTime.ALWAYS )
    @Column(columnDefinition =
        "AS CONCAT(" +
        "	COALESCE(firstName, ''), " +
        "	COALESCE(' ' + middleName1, ''), " +
        "	COALESCE(' ' + middleName2, ''), " +
        "	COALESCE(' ' + middleName3, ''), " +
        "	COALESCE(' ' + middleName4, ''), " +
        "	COALESCE(' ' + middleName5, ''), " +
        "	COALESCE(' ' + lastName, '') " +
        ")")
    private String fullName;

    //Getters and setters omitted for brevity

    public String getFullName() {
        return fullName;
    }
}

The fullName property is calculated using the SQL Server CONCAT function, by including all names. The COALESCE function is used because CONCAT does not take a separator in SQL Server.

The columnDefinition is only useful when generating the database schema from JPA annotations (which you shouldn’t do in a production environment). Because we provided a custom @Column definition, the underlying database table looks like this:

CREATE TABLE Hero
(
  id BIGINT NOT NULL ,
  firstName VARCHAR(255) ,
  fullName AS CONCAT(COALESCE(firstName, ''),
                     COALESCE(' ' + middleName1, ''),
                     COALESCE(' ' + middleName2, ''),
                     COALESCE(' ' + middleName3, ''),
                     COALESCE(' ' + middleName4, ''),
                     COALESCE(' ' + middleName5, ''),
                     COALESCE(' ' + lastName, '')) ,
  lastName VARCHAR(255) ,
  middleName1 VARCHAR(255) ,
  middleName2 VARCHAR(255) ,
  middleName3 VARCHAR(255) ,
  middleName4 VARCHAR(255) ,
  middleName5 VARCHAR(255) ,
  PRIMARY KEY ( id )
)

The @Generated annotation is used to instruct Hibernate when the associated column value is calculated, and it can take two values:

  • INSERT – meaning that the column value is calculated at insert time
  • ALWAYS – meaning that the column value is calculated both at insert and update time

Testing time

Now, when saving a Hero entity:

doInJPA( entityManager -> {
    Hero heroine = new Hero();
    heroine.setId( 1L );

    heroine.setFirstName( "Agustina" );
    heroine.setMiddleName1( "Raimunda" );
    heroine.setMiddleName2( "María" );
    heroine.setMiddleName3( "Saragossa" );
    heroine.setLastName( "Domènech" );

    entityManager.persist( heroine );
    LOGGER.info("After entity persist action");
    entityManager.flush();

    assertEquals(
        "Agustina Raimunda María Saragossa Domènech", 
        heroine.getFullName()
    );
} );

Hibernate generates the following SQL statements:

-- After entity persist action

INSERT INTO Hero 
    (firstName, lastName, middleName1, middleName2, middleName3, middleName4, middleName5, id) 
VALUES 
    ('Agustina', 'Domènech', 'Raimunda', 'María', 'Saragossa', NULL(VARCHAR), NULL(VARCHAR), 1)

SELECT 
    h.fullName as fullName3_0_ 
FROM Hero h 
WHERE h.id = 1

Note the SELECT query that is issued after the flush operation, which allows Hibernate to fetch the calculated entity property.

When updating and loading the entity:

doInJPA( entityManager -> {
    Hero heroine = entityManager.find( Hero.class, 1L );
    heroine.setMiddleName1( null );
    heroine.setMiddleName2( null );
    heroine.setMiddleName3( null );
    heroine.setLastName( "de Aragón" );

    LOGGER.info("After entity update action");
    entityManager.flush();

    assertEquals("Agustina de Aragón", heroine.getFullName());
} );

Hibernate generates the following SQL statements:

SELECT 
    h.id AS id1_0_0_,
    h.firstName AS firstNam2_0_0_,
    h.fullName AS fullName3_0_0_,
    h.lastName AS lastName4_0_0_,
    h.middleName1 AS middleNa5_0_0_,
    h.middleName2 AS middleNa6_0_0_,
    h.middleName3 AS middleNa7_0_0_,
    h.middleName4 AS middleNa8_0_0_,
    h.middleName5 AS middleNa9_0_0_
FROM Hero h
WHERE 
    h.id = 1

UPDATE Hero 
SET 
    firstName = 'Agustina', 
    lastName = 'de Aragón', 
    middleName1 = NULL(VARCHAR), 
    middleName2 = NULL(VARCHAR), 
    middleName3 = NULL(VARCHAR), 
    middleName4 = NULL(VARCHAR), 
    middleName5 = NULL(VARCHAR) 
WHERE 
    id = 1 
      
-- After entity update action

SELECT 
    h.fullName as fullName3_0_ 
FROM Hero h 
WHERE h.id = 1

Again, the last SELECT query is used to ensure that the managed entity is in sync with the underlying table row.

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

Mapping calculated entity properties is fairly easy with Hibernate, and you have multiple ways to achieve this goal.

The @Generated annotation allows you to build entity properties dynamically upon insert or update based on other property values.

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.