From 383ffa56eb0cf12893c86daa0eefda8da189c4fe Mon Sep 17 00:00:00 2001 From: Gavin King Date: Fri, 4 Nov 2022 11:21:18 +0100 Subject: [PATCH] HHH-15663 add writable member to @Generated annotation This is useful if you're using custom SQL, e.g. @SqlInsert. Also improve the Javadoc surrounding all this stuff. --- .../org/hibernate/annotations/Generated.java | 9 ++ .../org/hibernate/annotations/SQLDelete.java | 12 ++- .../org/hibernate/annotations/SQLInsert.java | 22 ++++- .../org/hibernate/annotations/SQLUpdate.java | 25 ++++- .../cfg/annotations/PropertyBinder.java | 2 +- .../java/org/hibernate/mapping/UniqueKey.java | 3 +- .../mapping/GeneratedValueResolver.java | 2 +- .../entity/AbstractEntityPersister.java | 4 +- .../tuple/GeneratedValueGeneration.java | 7 +- .../org/hibernate/tuple/ValueGeneration.java | 92 ++++++++++++++----- .../tuple/entity/EntityMetamodel.java | 2 +- 11 files changed, 141 insertions(+), 39 deletions(-) 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 9eae31eab4..2d92165c82 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Generated.java @@ -53,4 +53,13 @@ public @interface Generated { * */ GenerationTime value(); + + /** + * Determines if the column mapped by the annotated property is included in SQL + * {@code INSERT} and {@code UPDATE} statements. By default, it is excluded. + * + * @return {@code true} if the mapped column should be included in SQL + * {@code INSERT} and {@code UPDATE} statements. + */ + boolean writable() default false; } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SQLDelete.java b/hibernate-core/src/main/java/org/hibernate/annotations/SQLDelete.java index 49f57a2302..e759b628b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/SQLDelete.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SQLDelete.java @@ -16,10 +16,16 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Specifies a custom SQL DML statement to be used in place of the default SQL generated by - * Hibernate when an entity or collection row is deleted from the database. + * Specifies a custom SQL DML statement to be used in place of the default SQL + * generated by Hibernate when an entity or collection row is deleted from the + * database. + *

+ * The given {@link #sql SQL statement} must have exactly the number of JDBC + * {@code ?} parameters that Hibernate expects, in the exact order Hibernate + * expects. The primary key columns come before the version column if the + * entity is versioned. * - * @author L�szl� Benke + * @author Laszlo Benke */ @Target({TYPE, FIELD, METHOD}) @Retention(RUNTIME) diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SQLInsert.java b/hibernate-core/src/main/java/org/hibernate/annotations/SQLInsert.java index e1d5751fbb..ed2a2ca606 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/SQLInsert.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SQLInsert.java @@ -16,10 +16,26 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Specifies a custom SQL DML statement to be used in place of the default SQL generated by - * Hibernate when an entity or collection row is inserted in the database. + * Specifies a custom SQL DML statement to be used in place of the default SQL + * generated by Hibernate when an entity or collection row is inserted in the + * database. + *

+ * The given {@link #sql SQL statement} must have exactly the number of JDBC + * {@code ?} parameters that Hibernate expects, that is, one for each column + * mapped by the entity, in the exact order Hibernate expects. In particular, + * the {@link jakarta.persistence.Id primary key} columns must come last. + *

+ * If a column should not be written as part of the insert statement, + * and has no corresponding JDBC parameter in the custom SQL, it must be mapped + * using {@link jakarta.persistence.Column#insertable insertable=false}. + *

+ * A custom SQL insert statement might transform the column values as they + * are written. In this case, the state of the entity held in memory loses + * synchronization with the database after the insert is executed unless + * {@link Generated @Generated(value=INSERT, writable=true)} is specified, + * forcing Hibernate to reread the state of the entity after each insert. * - * @author L�szl� Benke + * @author Laszlo Benke */ @Target({TYPE, FIELD, METHOD}) @Retention(RUNTIME) diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SQLUpdate.java b/hibernate-core/src/main/java/org/hibernate/annotations/SQLUpdate.java index bf37502606..874f247533 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/SQLUpdate.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SQLUpdate.java @@ -16,10 +16,29 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Specifies a custom SQL DML statement to be used in place of the default SQL generated by - * Hibernate when an entity or collection row is updated in the database. + * Specifies a custom SQL DML statement to be used in place of the default SQL + * generated by Hibernate when an entity or collection row is updated in the + * database. + *

+ * The given {@link #sql SQL statement} must have exactly the number of JDBC + * {@code ?} parameters that Hibernate expects, that is, one for each column + * mapped by the entity, in the exact order Hibernate expects. In particular, + * the {@link jakarta.persistence.Id primary key} columns come last unless + * the entity is {@link jakarta.persistence.Version versioned}, in which case + * there must be a second JDBC parameter for the version column, which comes + * after the primary key. + *

+ * If a column should not be written as part of the update statement, + * and has no corresponding JDBC parameter in the custom SQL, it must be mapped + * using {@link jakarta.persistence.Column#updatable() updatable=false}. + *

+ * A custom SQL update statement might transform the column values as they + * are written. In this case, the state of the entity held in memory loses + * synchronization with the database after the update is executed unless + * {@link Generated @Generated(value=ALWAYS, writable=true)} is specified, + * forcing Hibernate to reread the state of the entity after each update. * - * @author L�szl� Benke + * @author Laszlo Benke */ @Target({TYPE, FIELD, METHOD}) @Retention(RUNTIME) diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java index e001c4bfbc..7f7afe973d 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/PropertyBinder.java @@ -390,7 +390,7 @@ public class PropertyBinder { return NoValueGeneration.INSTANCE; } - if ( valueGeneration.getValueGenerator() == null ) { + if ( !valueGeneration.writeColumn() ) { // if we have an in-db generator, mark it as not insertable nor updatable insertable = false; updatable = false; diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/UniqueKey.java b/hibernate-core/src/main/java/org/hibernate/mapping/UniqueKey.java index 7383893f52..526c940393 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/UniqueKey.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/UniqueKey.java @@ -14,7 +14,8 @@ import org.hibernate.engine.spi.Mapping; import org.hibernate.internal.util.StringHelper; /** - * A mapping model object representing a unique key constraint on a relational database table. + * A mapping model object representing a {@linkplain jakarta.persistence.UniqueConstraint unique key} + * constraint on a relational database table. * * @author Brett Meyer */ diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/GeneratedValueResolver.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/GeneratedValueResolver.java index 00c23ec168..8a5c234163 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/GeneratedValueResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/GeneratedValueResolver.java @@ -39,7 +39,7 @@ public interface GeneratedValueResolver { // value generation which we'll circle back to as we convert write operations to // use the "runtime mapping" (`org.hibernate.metamodel.mapping`) model - if ( valueGeneration.getValueGenerator() == null ) { + if ( valueGeneration.generatedByDatabase() ) { // in-db generation (column-default, function, etc) return new InDatabaseGeneratedValueResolver( requestedTiming, dbSelectionPosition ); } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index cbf5993d06..dfe1e38d99 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -2937,7 +2937,7 @@ public abstract class AbstractEntityPersister else { final ValueGeneration valueGeneration = attributeMapping.getValueGeneration(); if ( valueGeneration.getGenerationTiming().includesUpdate() - && valueGeneration.getValueGenerator() == null + && valueGeneration.generatedByDatabase() && valueGeneration.referenceColumnInSql() ) { update.addColumns( getPropertyColumnNames( index ), @@ -3059,7 +3059,7 @@ public abstract class AbstractEntityPersister else { final ValueGeneration valueGeneration = attributeMapping.getValueGeneration(); if ( valueGeneration.getGenerationTiming().includesInsert() - && valueGeneration.getValueGenerator() == null + && valueGeneration.generatedByDatabase() && valueGeneration.referenceColumnInSql() ) { insert.addColumns( getPropertyColumnNames( index ), diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/GeneratedValueGeneration.java b/hibernate-core/src/main/java/org/hibernate/tuple/GeneratedValueGeneration.java index 80d0523167..e508d2352e 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/GeneratedValueGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/GeneratedValueGeneration.java @@ -17,6 +17,7 @@ import org.hibernate.annotations.Generated; public class GeneratedValueGeneration implements AnnotationValueGeneration { private GenerationTiming timing; + private boolean writable; public GeneratedValueGeneration() { } @@ -27,7 +28,8 @@ public class GeneratedValueGeneration implements AnnotationValueGeneration propertyType) { - this.timing = annotation.value().getEquivalent(); + timing = annotation.value().getEquivalent(); + writable = annotation.writable(); } @Override @@ -43,8 +45,7 @@ public class GeneratedValueGeneration implements AnnotationValueGeneration + *

  • {@linkplain GenerationTiming#INSERT when the entity is inserted}, + *
  • {@linkplain GenerationTiming#ALWAYS whenever the entity is inserted or updated}, or + *
  • {@linkplain GenerationTiming#NEVER never}. + * * - * @return When the value is generated. + * @return The {@link GenerationTiming} specifying when the value is generated. */ GenerationTiming getGenerationTiming(); /** - * Obtain the in-VM value generator. - *

    - * May return {@code null}. In fact for values that are generated "in the database" via execution of the - * INSERT/UPDATE statement, the expectation is that {@code null} be returned here + * Obtain the {@linkplain ValueGenerator Java value generator}, if the value is generated in + * Java, or return {@code null} if the value is generated by the database. * - * @return The strategy for performing in-VM value generation + * @return The value generator */ ValueGenerator getValueGenerator(); /** - * For values which are generated in the database ({@link #getValueGenerator()} == {@code null}), should the - * column be referenced in the INSERT / UPDATE SQL? - *

    - * This will be false most often to have a DDL-defined DEFAULT value be applied on INSERT + * Determines if the column whose value is generated is included in the column list of the + * SQL {@code insert} or {@code update} statement, in the case where the value is generated + * by the database. For example, this method should return: + *

    + * If the value is generated in Java, this method is not called, and so for backward + * compatibility with Hibernate 5 it is permitted to return any value. On the other hand, + * when a property value is generated in Java, the column certainly must be included in the + * column list, and so it's most correct for this method to return {@code true}! * - * @return {@code true} indicates the column should be included in the SQL. + * @return {@code true} if the column is included in the column list of the SQL statement. */ boolean referenceColumnInSql(); /** - * For values which are generated in the database ({@link #getValueGenerator} == {@code null}), if the - * column will be referenced in the SQL ({@link #referenceColumnInSql()} == {@code true}), what value should be - * used in the SQL as the column value. - *

    - * Generally this will be a function call or a marker (DEFAULTS). - *

    - * NOTE : for in-VM generation, this will not be called and the column value will implicitly be a JDBC parameter ('?') + * A SQL expression indicating how to calculate the generated value when the property value + * is {@linkplain #generatedByDatabase() generated in the database} and the mapped column is + * {@linkplain #referenceColumnInSql() included in the SQL statement}. The SQL expression + * might be: + *

    + * When the property value is generated in Java, this method is not called, and its value is + * implicitly the string {@code "?"}, that is, a JDBC parameter to which the generated value + * is bound. * - * @return The column value to be used in the SQL. + * @return The column value to be used in the generated SQL statement. */ String getDatabaseGeneratedReferencedColumnValue(); + + /** + * Determines if the property value is generated in Java, or by the database. + *

    + * This default implementation returns true if the {@linkplain #getValueGenerator() Java + * value generator} is {@code null}. + * + * @return {@code true} if the value is generated by the database, or false if it is + * generated in Java using a {@link ValueGenerator}. + */ + default boolean generatedByDatabase() { + return getValueGenerator() == null; + } + + /** + * Determines if the property value is written to JDBC as the argument of a JDBC {@code ?} + * parameter. This is the case when either: + *

    + */ + default boolean writeColumn() { + return !generatedByDatabase() // value generated in memory and then written as normal + // current value of property of entity instance written completely as normal + || referenceColumnInSql() && getDatabaseGeneratedReferencedColumnValue()==null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index 6e3ecdcf92..fc6ac9427b 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -449,7 +449,7 @@ public class EntityMetamodel implements Serializable { final ValueGeneration valueGeneration = mappingProperty.getValueGenerationStrategy(); if ( valueGeneration != null && valueGeneration.getGenerationTiming() != GenerationTiming.NEVER ) { // the property is generated in full. build the generation strategy pair. - if ( valueGeneration.getValueGenerator() != null ) { + if ( !valueGeneration.generatedByDatabase() ) { // in-memory generation return new GenerationStrategyPair( FullInMemoryValueGenerationStrategy.create( valueGeneration )