How a simple database trigger can break @GeneratedValue in JPA / Hibernate.

Photo by Kolby Milton, https://unsplash.com/de/fotos/blau-gelbe-nerf-pistole-njOuW7E1y5g

After updating an application from Spring Boot 2 to 3, I ran into errors of the following type when trying to insert a new value into the database (Microsoft SQL Server):

java.lang.IllegalArgumentException: 
Can not set int field systems.enji.DemoEntity.id to null value
org.hibernate.AssertionFailure: null identifier (systems.enji.DemoEntity)
  at org.hibernate.engine.spi.EntityKey.<init>(EntityKey.java:52)

The referenced piece of code auto-generates an ID using a database identity column:

@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

It turned out that the reason for the error was a database trigger that replaced the original insert with a slightly modified one. In fact, even a simple trigger such as the following (which only passes through the values of the original insert, without any additional logic) prevents Hibernate from retrieving the auto-generated value:

CREATE TRIGGER pass_through_trigger 
ON FOO_TABLE
INSTEAD OF INSERT
AS
INSERT INTO FOO_TABLE(COL1, COL2)
SELECT COL1, COL2 FROM INSERTED;

In Hibernate 5 (Spring Boot 2), this only meant that the ID was not immediately available in the managed entity, but the insert succeeded nonetheless. However, Hibernate 6 (Spring Boot 3) complains about this with one of the errors mentioned above.

To make sure that the auto-generated value is handed over to Hibernate, the trigger has to be extended with selecting SCOPE_IDENTITY():

CREATE TRIGGER pass_through_trigger 
ON FOO_TABLE
INSTEAD OF INSERT
AS
BEGIN
  INSERT INTO FOO_TABLE(COL1, COL2)
    SELECT COL1, COL2 FROM INSERTED;
  SELECT SCOPE_IDENTITY();
END;