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 timeALWAYS
– 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 11th of October about High-Performance SQL.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.
