From 731a5dbdd52291bdca11bea22a7dfd656fe49307 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Tue, 5 Nov 2024 19:40:16 +0100 Subject: [PATCH] HHH-18815 clarify and test semantics of @Generated and @Immutable Signed-off-by: Gavin King --- .../org/hibernate/annotations/Generated.java | 13 +++ .../sql/ImmutableSqlGeneratedTest.java | 104 ++++++++++++++++++ .../generated/sql/SqlGeneratedTest.java | 1 - 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/ImmutableSqlGeneratedTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java b/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java index 71b24a4de9..405cecc622 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java @@ -48,12 +48,25 @@ import static org.hibernate.generator.EventType.INSERT; * {@link GeneratedColumn} annotation, so that Hibernate automatically * generates the correct DDL. * + *

+ * A {@code @Generated} field may be generated on + * {@linkplain EventType#INSERT inserts}, on + * {@linkplain EventType#UPDATE updates}, or on both inserts and updates, + * as specified by the {@link #event} member. + * By default, {@code @Generated} fields are not immutable, and so a field + * which is generated on insert may later be explicitly assigned a new value + * by the application program, resulting in its value being updated in the + * database. If this is not desired, the {@link Immutable @Immutable} + * annotation may be used in conjunction with {@code @Generated} to specify + * that the field may never be updated after initial generation of its value. * * @author Emmanuel Bernard * * @see jakarta.persistence.GeneratedValue * @see ColumnDefault * @see GeneratedColumn + * @see Formula + * @see Immutable */ @ValueGenerationType( generatedBy = GeneratedGeneration.class ) @IdGeneratorType( GeneratedGeneration.class ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/ImmutableSqlGeneratedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/ImmutableSqlGeneratedTest.java new file mode 100644 index 0000000000..1d4d647ae0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/ImmutableSqlGeneratedTest.java @@ -0,0 +1,104 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.generated.sql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.Immutable; +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.generator.EventType; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +/** + * @author Gavin King + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = ImmutableSqlGeneratedTest.OrderLine.class) +@SessionFactory +@SkipForDialect(dialectClass = SybaseASEDialect.class, + reason = "The name 'current_timestamp' is illegal in this context. Only constants, constant expressions, or variables allowed here.") +public class ImmutableSqlGeneratedTest { + + @Test + public void test(SessionFactoryScope scope) { + BigDecimal unitPrice = new BigDecimal("12.99"); + scope.inTransaction( session -> { + OrderLine entity = new OrderLine( unitPrice, 5 ); + session.persist(entity); + session.flush(); + assertEquals( "new", entity.status ); + assertEquals( unitPrice, entity.unitPrice ); + assertEquals( 5, entity.quantity ); + assertNotNull( entity.updated ); + } ); + scope.inTransaction( session -> { + OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult(); + assertEquals( unitPrice, entity.unitPrice ); + assertEquals( 5, entity.quantity ); + assertEquals( "new", entity.status ); + assertNotNull( entity.updated ); + entity.status = "old"; + } ); + scope.inTransaction( session -> { + OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult(); + assertEquals( unitPrice, entity.unitPrice ); + assertEquals( 5, entity.quantity ); + assertEquals( "new", entity.status ); + assertNotNull( entity.updated ); + LocalDateTime previous = entity.updated; + entity.quantity = 10; + entity.status = "old"; + session.flush(); + assertNotNull( entity.updated ); + assertFalse( previous == entity.updated ); + } ); + scope.inTransaction( session -> { + OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult(); + assertEquals( unitPrice, entity.unitPrice ); + assertEquals( 10, entity.quantity ); + assertEquals( "new", entity.status ); + assertNotNull( entity.updated ); + } ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createQuery( "delete WithDefault" ).executeUpdate() ); + } + + @Entity(name="WithDefault") + public static class OrderLine { + @Id + private long id; + private BigDecimal unitPrice; + private int quantity = 1; + @Immutable + @Generated(sql = "'new'") + private String status; + @Generated(event = {EventType.INSERT, EventType.UPDATE}, + sql = "current_timestamp") + private LocalDateTime updated; + + + public OrderLine() {} + public OrderLine(BigDecimal unitPrice, int quantity) { + this.unitPrice = unitPrice; + this.quantity = quantity; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/SqlGeneratedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/SqlGeneratedTest.java index a9983081f3..97282e6258 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/SqlGeneratedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/sql/SqlGeneratedTest.java @@ -52,7 +52,6 @@ public class SqlGeneratedTest { assertEquals( "new", entity.status ); assertNotNull( entity.updated ); entity.status = "old"; - session.flush(); } ); scope.inTransaction( session -> { OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult();