From 8a78cf6b30df55a54709ec9178bf567b688d77f4 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 4 Nov 2024 20:48:58 +0100 Subject: [PATCH 1/4] HHH-18809 support @Formula @Generated Signed-off-by: Gavin King --- .../ast/tree/expression/ColumnReference.java | 28 +++---- .../formula/FormulaGeneratedTest.java | 80 +++++++++++++++++++ 2 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java index 1be3b4e7ba63..f130ac493587 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java @@ -10,7 +10,6 @@ import java.util.Objects; import java.util.function.Consumer; -import org.hibernate.internal.util.StringHelper; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectablePath; @@ -22,6 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.internal.util.StringHelper.nullIfEmpty; import static org.hibernate.internal.util.StringHelper.replace; import static org.hibernate.sql.Template.TEMPLATE; @@ -116,21 +116,20 @@ public ColumnReference( boolean isFormula, String customReadExpression, JdbcMapping jdbcMapping) { - this.qualifier = StringHelper.nullIfEmpty( qualifier ); + this.qualifier = nullIfEmpty( qualifier ); if ( isFormula ) { - assert qualifier != null; - this.columnExpression = replace( columnExpression, TEMPLATE, qualifier ); + this.columnExpression = qualifier == null + ? replace( columnExpression, TEMPLATE + '.', "" ) + : replace( columnExpression, TEMPLATE, qualifier ); } else { this.columnExpression = columnExpression; } - if ( selectablePath == null ) { - this.selectablePath = new SelectablePath( this.columnExpression ); - } - else { - this.selectablePath = selectablePath; - } + + this.selectablePath = selectablePath == null + ? new SelectablePath( this.columnExpression ) + : selectablePath; this.isFormula = isFormula; this.readExpression = customReadExpression; @@ -181,12 +180,9 @@ public void appendReadExpression(String qualifier, Consumer appender) { appender.accept( columnExpression ); } else if ( readExpression != null ) { - if ( qualifier == null ) { - appender.accept( replace( readExpression, TEMPLATE + ".", "" ) ); - } - else { - appender.accept( replace( readExpression, TEMPLATE, qualifier ) ); - } + appender.accept( qualifier == null + ? replace( readExpression, TEMPLATE + '.', "" ) + : replace( readExpression, TEMPLATE, qualifier ) ); } else { if ( qualifier != null ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java new file mode 100644 index 000000000000..a7707894398c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.generated.formula; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.Formula; +import org.hibernate.annotations.Generated; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.hibernate.cfg.JdbcSettings.USE_GET_GENERATED_KEYS; +import static org.junit.Assert.assertEquals; + +/** + * @author Gavin King + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = FormulaGeneratedTest.OrderLine.class) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = USE_GET_GENERATED_KEYS, value = "false")) +public class FormulaGeneratedTest { + + @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 ); + } ); + scope.inTransaction( session -> { + OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult(); + assertEquals( unitPrice, entity.unitPrice ); + assertEquals( 5, entity.quantity ); + assertEquals( "new", entity.status ); + entity.status = "old"; //should be ignored when fetch=true + } ); + scope.inTransaction( session -> { + OrderLine entity = session.createQuery("from WithDefault", OrderLine.class ).getSingleResult(); + assertEquals( unitPrice, entity.unitPrice ); + assertEquals( 5, entity.quantity ); + assertEquals( "new", entity.status ); + } ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createQuery( "delete WithDefault" ).executeUpdate() ); + } + + @Entity(name="WithDefault") + public static class OrderLine { + @Id + private BigDecimal unitPrice; + @Id + private int quantity = 1; + @Generated + @Formula(value = "'new'") + private String status; + + public OrderLine() {} + public OrderLine(BigDecimal unitPrice, int quantity) { + this.unitPrice = unitPrice; + this.quantity = quantity; + } + } +} From b732a409e5a3e00ed0c11e87faf22a170b9d8d20 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 4 Nov 2024 20:49:16 +0100 Subject: [PATCH 2/4] minor changes Signed-off-by: Gavin King --- .../org/hibernate/id/insert/InsertReturningDelegate.java | 8 ++++---- .../id/insert/SybaseJConnGetGeneratedKeysDelegate.java | 8 ++++---- .../src/main/java/org/hibernate/mapping/BasicValue.java | 1 - 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java index 93f7bbe17046..17892d35bde4 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/InsertReturningDelegate.java @@ -96,10 +96,10 @@ protected GeneratedValues executeAndExtractReturning( @Override public String prepareIdentifierGeneratingInsert(String insertSQL) { - return dialect().getIdentityColumnSupport().appendIdentitySelectToInsert( - ( (BasicEntityIdentifierMapping) persister.getRootEntityDescriptor().getIdentifierMapping() ).getSelectionExpression(), - insertSQL - ); + final BasicEntityIdentifierMapping identifierMapping = + (BasicEntityIdentifierMapping) persister.getRootEntityDescriptor().getIdentifierMapping(); + return dialect().getIdentityColumnSupport() + .appendIdentitySelectToInsert( identifierMapping.getSelectionExpression(), insertSQL ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java index 6a72a2056187..6f1b93cc6752 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/SybaseJConnGetGeneratedKeysDelegate.java @@ -33,10 +33,10 @@ public SybaseJConnGetGeneratedKeysDelegate(EntityPersister persister) { @Override public String prepareIdentifierGeneratingInsert(String insertSQL) { - return dialect().getIdentityColumnSupport().appendIdentitySelectToInsert( - ( (BasicEntityIdentifierMapping) persister.getRootEntityDescriptor().getIdentifierMapping() ).getSelectionExpression(), - insertSQL - ); + final BasicEntityIdentifierMapping identifierMapping = + (BasicEntityIdentifierMapping) persister.getRootEntityDescriptor().getIdentifierMapping(); + return dialect().getIdentityColumnSupport() + .appendIdentitySelectToInsert( identifierMapping.getSelectionExpression(), insertSQL ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index 9c007ed6ed51..d4ba9064cbf6 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -34,7 +34,6 @@ import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; -import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.Size; From 9b10a49a9aec9594d46fd810250eb15a2e9e236e Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 4 Nov 2024 22:01:35 +0100 Subject: [PATCH 3/4] HHH-18809 auto-disable use of getGeneratedKeys() when there is a @Formula Signed-off-by: Gavin King --- .../values/internal/GeneratedValuesHelper.java | 10 ++++++++-- .../generated/formula/FormulaGeneratedTest.java | 5 +---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesHelper.java b/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesHelper.java index 28775eaf5be8..2bb21a814f49 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesHelper.java @@ -27,6 +27,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.mutation.EntityTableMapping; import org.hibernate.pretty.MessageHelper; @@ -326,7 +327,11 @@ private static List getActualGeneratedModelParts( public static GeneratedValuesMutationDelegate getGeneratedValuesDelegate( EntityPersister persister, EventType timing) { - final boolean hasGeneratedProperties = !persister.getGeneratedProperties( timing ).isEmpty(); + final List generatedProperties = persister.getGeneratedProperties( timing ); + final boolean hasGeneratedProperties = !generatedProperties.isEmpty(); + final boolean hasFormula = + generatedProperties.stream() + .anyMatch( part -> part instanceof SelectableMapping selectable && selectable.isFormula() ); final boolean hasRowId = timing == EventType.INSERT && persister.getRowIdMapping() != null; final Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); @@ -341,7 +346,8 @@ && noCustomSql( persister, timing ) ) { return null; } - if ( dialect.supportsInsertReturningGeneratedKeys() + if ( !hasFormula + && dialect.supportsInsertReturningGeneratedKeys() && persister.getFactory().getSessionFactoryOptions().isGetGeneratedKeysEnabled() ) { return new GetGeneratedKeysDelegate( persister, false, timing ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java index a7707894398c..5f8a4c7f81e5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/formula/FormulaGeneratedTest.java @@ -9,16 +9,13 @@ import org.hibernate.annotations.Formula; import org.hibernate.annotations.Generated; import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; -import org.hibernate.testing.orm.junit.Setting; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import java.math.BigDecimal; -import static org.hibernate.cfg.JdbcSettings.USE_GET_GENERATED_KEYS; import static org.junit.Assert.assertEquals; /** @@ -27,7 +24,7 @@ @SuppressWarnings("JUnitMalformedDeclaration") @DomainModel(annotatedClasses = FormulaGeneratedTest.OrderLine.class) @SessionFactory -@ServiceRegistry(settings = @Setting(name = USE_GET_GENERATED_KEYS, value = "false")) +//@ServiceRegistry(settings = @Setting(name = USE_GET_GENERATED_KEYS, value = "false")) public class FormulaGeneratedTest { @Test From a07ba508dd86f2314ecb2316f075c43237cee6f3 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 4 Nov 2024 22:10:21 +0100 Subject: [PATCH 4/4] HHH-18809 update Javadoc with this usage Signed-off-by: Gavin King --- .../main/java/org/hibernate/annotations/Formula.java | 9 +++++++++ .../main/java/org/hibernate/annotations/Generated.java | 10 ++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java b/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java index c8cc6e9963b0..3ee1ab2eb9f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Formula.java @@ -32,6 +32,15 @@ * Character middleInitial; * *

+ * By default, the fields of an entity are not updated with the results of evaluating + * the formula after an {@code insert} or {@code update}. The {@link Generated @Generated} + * annotation may be used to specify that this should happen: + *

+ * @Generated  // evaluate the formula after an insert
+ * @Formula("sub_total * (1.0 + tax)")
+ * BigDecimal totalWithTax;
+ * 
+ *

* For an entity with {@linkplain jakarta.persistence.SecondaryTable secondary tables}, * a formula may involve columns of the primary table, or columns of any one of the * secondary tables. But it may not involve columns of more than one table. 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 2dd3d61f88dc..71b24a4de995 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java @@ -31,11 +31,13 @@ *

  • a mapped column has a default value defined in DDL, in which case * {@code @Generated} is used in conjunction with {@link ColumnDefault}, *
  • a {@linkplain #sql() SQL expression} is used to compute the value of - * a mapped column, or - *
  • when a custom SQL {@link SQLInsert insert} or {@link SQLUpdate update} + * a mapped column, + *
  • a custom SQL {@link SQLInsert insert} or {@link SQLUpdate update} * statement specified by an entity assigns a value to the annotated - * property of the entity, or {@linkplain #writable() transforms} the - * value currently assigned to the annotated property. + * property of the entity, or {@linkplain #writable transforms} the + * value currently assigned to the annotated property, or + *
  • there is no mapped column, and the value of the field is determined + * by evaluating a SQL {@link Formula}. * *

    * On the other hand: