From 61c128000b0334fc11ab45057275856d29dff94b Mon Sep 17 00:00:00 2001 From: Gavin King Date: Fri, 4 Nov 2022 17:48:54 +0100 Subject: [PATCH] HHH-15672 introduce Generated(UPDATE) for properties only generated on update --- .../dialect/SybaseLegacyDialect.java | 5 + .../hibernate/annotations/GenerationTime.java | 4 + .../source/internal/hbm/ModelBinder.java | 22 ++- .../cfg/annotations/PropertyBinder.java | 17 ++- .../java/org/hibernate/dialect/Dialect.java | 11 +- .../org/hibernate/dialect/SybaseDialect.java | 5 + .../mapping/GeneratedValueResolver.java | 15 +- .../internal/GeneratedValuesProcessor.java | 3 +- .../entity/AbstractEntityPersister.java | 8 +- .../org/hibernate/tuple/GenerationTiming.java | 22 +++ .../org/hibernate/tuple/ValueGeneration.java | 24 +++ .../tuple/entity/EntityMetamodel.java | 74 +++++++--- .../DatabaseTimestampsColumnTest.java | 138 ++++++++++++++++++ .../metadata/AuditMetadataGenerator.java | 3 +- .../orm/junit/DialectFeatureChecks.java | 2 +- 15 files changed, 297 insertions(+), 56 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/annotations/DatabaseTimestampsColumnTest.java diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java index a57c15fc87..ecbde29c12 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacyDialect.java @@ -314,6 +314,11 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect { throw new UnsupportedOperationException( "format() function not supported on Sybase"); } + @Override + public boolean supportsStandardCurrentTimestampFunction() { + return false; + } + @Override public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) throws SQLException { diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/GenerationTime.java b/hibernate-core/src/main/java/org/hibernate/annotations/GenerationTime.java index 5f3eda2c74..d0b925ca4f 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/GenerationTime.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/GenerationTime.java @@ -25,6 +25,10 @@ public enum GenerationTime { * Indicates the value is generated on insert. */ INSERT( GenerationTiming.INSERT ), + /** + * Indicates the value is generated on update. + */ + UPDATE( GenerationTiming.UPDATE ), /** * Indicates the value is generated on insert and on update. */ diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java index 8eeb02696f..6c54aa0cba 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java @@ -998,11 +998,17 @@ public class ModelBinder { // generated; aka, "insert" is invalid; this is dis-allowed by the DTD, // but just to make sure... if ( prop.getValueGenerationStrategy() != null ) { - if ( prop.getValueGenerationStrategy().getGenerationTiming() == GenerationTiming.INSERT ) { - throw new MappingException( - "'generated' attribute cannot be 'insert' for version/timestamp property", - sourceDocument.getOrigin() - ); + switch ( prop.getValueGenerationStrategy().getGenerationTiming() ) { + case INSERT: + throw new MappingException( + "'generated' attribute cannot be 'insert' for version/timestamp property", + sourceDocument.getOrigin() + ); + case UPDATE: + throw new MappingException( + "'generated' attribute cannot be 'update' for version/timestamp property", + sourceDocument.getOrigin() + ); } } @@ -2531,13 +2537,13 @@ public class ModelBinder { property.setLazy( singularAttributeSource.isBytecodeLazy() ); final GenerationTiming generationTiming = singularAttributeSource.getGenerationTiming(); - if ( generationTiming == GenerationTiming.ALWAYS || generationTiming == GenerationTiming.INSERT ) { + if ( generationTiming != null && generationTiming != GenerationTiming.NEVER ) { // we had generation specified... // HBM only supports "database generated values" property.setValueGenerationStrategy( new GeneratedValueGeneration( generationTiming ) ); // generated properties can *never* be insertable... - if ( property.isInsertable() ) { + if ( property.isInsertable() && generationTiming.includesInsert() ) { log.debugf( "Property [%s] specified %s generation, setting insertable to false : %s", propertySource.getName(), @@ -2548,7 +2554,7 @@ public class ModelBinder { } // properties generated on update can never be updatable... - if ( property.isUpdateable() && generationTiming == GenerationTiming.ALWAYS ) { + if ( property.isUpdateable() && generationTiming.includesUpdate() ) { log.debugf( "Property [%s] specified ALWAYS generation, setting updateable to false : %s", propertySource.getName(), 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 644d476481..941295d737 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 @@ -434,14 +434,19 @@ public class PropertyBinder { final Class> generationType = generatorAnnotation.generatedBy(); final AnnotationValueGeneration valueGeneration = instantiateAndInitializeValueGeneration( annotation, generationType, property ); - if ( annotation.annotationType() == Generated.class - && property.isAnnotationPresent(Version.class) - && valueGeneration.getGenerationTiming() == GenerationTiming.INSERT ) { + if ( annotation.annotationType() == Generated.class && property.isAnnotationPresent(Version.class) ) { + switch ( valueGeneration.getGenerationTiming() ) { + case INSERT: + throw new AnnotationException("Property '" + qualify( holder.getPath(), name ) + + "' is annotated '@Generated(INSERT)' and '@Version' (use '@Generated(ALWAYS)' instead)" - throw new AnnotationException( "Property '" + qualify( holder.getPath(), name ) - + "' is annotated '@Generated(INSERT)' and '@Version' (use '@Generated(ALWAYS)' instead)" + ); + case UPDATE: + throw new AnnotationException("Property '" + qualify( holder.getPath(), name ) + + "' is annotated '@Generated(UPDATE)' and '@Version' (use '@Generated(ALWAYS)' instead)" - ); + ); + } } return valueGeneration; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index e1ef71049b..baaef6a831 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -2142,8 +2142,8 @@ public abstract class Dialect implements ConversionContext { /** * Should the value returned by {@link #getCurrentTimestampSelectString} - * be treated as callable. Typically this indicates that JDBC escape - * syntax is being used... + * be treated as callable. Typically, this indicates that JDBC escape + * syntax is being used. * * @return True if the {@link #getCurrentTimestampSelectString} return * is callable; false otherwise. @@ -2162,6 +2162,13 @@ public abstract class Dialect implements ConversionContext { throw new UnsupportedOperationException( "Database not known to define a current timestamp function" ); } + /** + * Does this database have an ANSI-SQL {@code current_timestamp} function? + */ + public boolean supportsStandardCurrentTimestampFunction() { + return true; + } + // SQLException support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java index 7980a5e49a..704d91e8ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseDialect.java @@ -324,6 +324,11 @@ public class SybaseDialect extends AbstractTransactSQLDialect { throw new UnsupportedOperationException( "format() function not supported on Sybase"); } + @Override + public boolean supportsStandardCurrentTimestampFunction() { + return false; + } + @Override public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) throws SQLException { 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 8a5c234163..0d3b17bdb4 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 @@ -25,26 +25,21 @@ public interface GeneratedValueResolver { int dbSelectionPosition) { assert requestedTiming != GenerationTiming.NEVER; - if ( valueGeneration == null || valueGeneration.getGenerationTiming().includes( GenerationTiming.NEVER ) ) { + if ( valueGeneration == null || !valueGeneration.getGenerationTiming().includes( requestedTiming ) ) { return NoGeneratedValueResolver.INSTANCE; } - - if ( requestedTiming == GenerationTiming.ALWAYS && valueGeneration.getGenerationTiming() == GenerationTiming.INSERT ) { - return NoGeneratedValueResolver.INSTANCE; - } - // todo (6.x) : incorporate `org.hibernate.tuple.InDatabaseValueGenerationStrategy` // and `org.hibernate.tuple.InMemoryValueGenerationStrategy` from `EntityMetamodel`. // this requires unification of the read and write (insert/update) aspects of // 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.generatedByDatabase() ) { + else if ( valueGeneration.generatedByDatabase() ) { // in-db generation (column-default, function, etc) return new InDatabaseGeneratedValueResolver( requestedTiming, dbSelectionPosition ); } - - return new InMemoryGeneratedValueResolver( valueGeneration.getValueGenerator(), requestedTiming ); + else { + return new InMemoryGeneratedValueResolver( valueGeneration.getValueGenerator(), requestedTiming ); + } } GenerationTiming getGenerationTiming(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java index 9a5b875568..31b0c1f529 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java @@ -66,7 +66,8 @@ public class GeneratedValuesProcessor { .getEntityMetamodel() .getInDatabaseValueGenerationStrategies(); entityDescriptor.visitAttributeMappings( mapping -> { - final InDatabaseValueGenerationStrategy inDatabaseValueGenerationStrategy = inDatabaseValueGenerationStrategies[mapping.getStateArrayPosition()]; + final InDatabaseValueGenerationStrategy inDatabaseValueGenerationStrategy = + inDatabaseValueGenerationStrategies[ mapping.getStateArrayPosition() ]; if ( inDatabaseValueGenerationStrategy.getGenerationTiming() == GenerationTiming.NEVER ) { return; } 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 28ea34f140..ceef38f99c 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 @@ -2938,10 +2938,11 @@ public abstract class AbstractEntityPersister if ( valueGeneration.getGenerationTiming().includesUpdate() && valueGeneration.generatedByDatabase() && valueGeneration.referenceColumnInSql() ) { + final Dialect dialect = getFactory().getJdbcServices().getDialect(); update.addColumns( getPropertyColumnNames( index ), SINGLE_TRUE, - new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue() } + new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) } ); hasColumns = true; } @@ -3060,10 +3061,11 @@ public abstract class AbstractEntityPersister if ( valueGeneration.getGenerationTiming().includesInsert() && valueGeneration.generatedByDatabase() && valueGeneration.referenceColumnInSql() ) { + final Dialect dialect = getFactory().getJdbcServices().getDialect(); insert.addColumns( getPropertyColumnNames( index ), SINGLE_TRUE, - new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue() } + new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) } ); } } @@ -5766,7 +5768,7 @@ public abstract class AbstractEntityPersister insertGeneratedValuesProcessor = createGeneratedValuesProcessor( GenerationTiming.INSERT ); } if ( hasUpdateGeneratedProperties() ) { - updateGeneratedValuesProcessor = createGeneratedValuesProcessor( GenerationTiming.ALWAYS ); + updateGeneratedValuesProcessor = createGeneratedValuesProcessor( GenerationTiming.UPDATE ); } staticFetchableList = new ArrayList<>( attributeMappings.size() ); visitSubTypeAttributeMappings( attributeMapping -> staticFetchableList.add( attributeMapping ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/GenerationTiming.java b/hibernate-core/src/main/java/org/hibernate/tuple/GenerationTiming.java index 4033bd61a4..dbcaa24b92 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/GenerationTiming.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/GenerationTiming.java @@ -53,6 +53,25 @@ public enum GenerationTiming { return timing.includesInsert(); } }, + /** + * Value generation that occurs when a row is updated in the database. + */ + UPDATE { + @Override + public boolean includesInsert() { + return false; + } + + @Override + public boolean includesUpdate() { + return true; + } + + @Override + public boolean includes(GenerationTiming timing) { + return timing.includesUpdate(); + } + }, /** * Value generation that occurs when a row is inserted or updated in the database. */ @@ -88,6 +107,9 @@ public enum GenerationTiming { if ( "insert".equalsIgnoreCase( name ) ) { return INSERT; } + else if ( "update".equalsIgnoreCase( name ) ) { + return UPDATE; + } else if ( "always".equalsIgnoreCase( name ) ) { return ALWAYS; } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java b/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java index eec99c431f..11ad5a5e98 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/ValueGeneration.java @@ -6,6 +6,8 @@ */ package org.hibernate.tuple; +import org.hibernate.dialect.Dialect; + import java.io.Serializable; /** @@ -33,6 +35,7 @@ public interface ValueGeneration extends Serializable { * Specifies that the property value is generated: * @@ -86,6 +89,27 @@ public interface ValueGeneration extends Serializable { */ String getDatabaseGeneratedReferencedColumnValue(); + /** + * 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. + * + * @param dialect The {@linkplain Dialect SQL dialect}, allowing generation of an expression + * in dialect-specific SQL. + * @return The column value to be used in the generated SQL statement. + */ + default String getDatabaseGeneratedReferencedColumnValue(Dialect dialect) { + return getDatabaseGeneratedReferencedColumnValue(); + } + /** * Determines if the property value is generated in Java, or by the database. *

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 fc6ac9427b..ba48119c69 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 @@ -23,6 +23,7 @@ import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper; import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cfg.NotYetImplementedException; +import org.hibernate.dialect.Dialect; import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadeStyles; @@ -307,24 +308,33 @@ public class EntityMetamodel implements Serializable { final ValueGenerator generator = pair.getInMemoryStrategy().getValueGenerator(); if ( generator != null ) { // we have some level of generation indicated - if ( timing == GenerationTiming.INSERT ) { - foundPreInsertGeneratedValues = true; - } - else if ( timing == GenerationTiming.ALWAYS ) { - foundPreInsertGeneratedValues = true; - foundPreUpdateGeneratedValues = true; + switch ( timing ) { + case INSERT: + foundPreInsertGeneratedValues = true; + break; + case UPDATE: + foundPreUpdateGeneratedValues = true; + break; + case ALWAYS: + foundPreInsertGeneratedValues = true; + foundPreUpdateGeneratedValues = true; + break; } } } } if ( pair.getInDatabaseStrategy() != null ) { - final GenerationTiming timing = pair.getInDatabaseStrategy().getGenerationTiming(); - if ( timing == GenerationTiming.INSERT ) { - foundPostInsertGeneratedValues = true; - } - else if ( timing == GenerationTiming.ALWAYS ) { - foundPostInsertGeneratedValues = true; - foundPostUpdateGeneratedValues = true; + switch ( pair.getInDatabaseStrategy().getGenerationTiming() ) { + case INSERT: + foundPostInsertGeneratedValues = true; + break; + case UPDATE: + foundPostUpdateGeneratedValues = true; + break; + case ALWAYS: + foundPostInsertGeneratedValues = true; + foundPostUpdateGeneratedValues = true; + break; } } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -487,20 +497,21 @@ public class EntityMetamodel implements Serializable { } public static InDatabaseValueGenerationStrategyImpl create( - SessionFactoryImplementor sessionFactoryImplementor, + SessionFactoryImplementor factory, Property mappingProperty, ValueGeneration valueGeneration) { - final int numberOfMappedColumns = mappingProperty.getType().getColumnSpan( sessionFactoryImplementor ); + final int numberOfMappedColumns = mappingProperty.getType().getColumnSpan( factory ); + final Dialect dialect = factory.getJdbcServices().getDialect(); if ( numberOfMappedColumns == 1 ) { return new InDatabaseValueGenerationStrategyImpl( valueGeneration.getGenerationTiming(), valueGeneration.referenceColumnInSql(), - new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue() } + new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) } ); } else { - if ( valueGeneration.getDatabaseGeneratedReferencedColumnValue() != null ) { + if ( valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) != null ) { LOG.debugf( "Value generator specified column value in reference to multi-column attribute [%s -> %s]; ignoring", mappingProperty.getPersistentClass(), @@ -625,7 +636,7 @@ public class EntityMetamodel implements Serializable { } // the base-line values for the aggregated InDatabaseValueGenerationStrategy we will build here. - GenerationTiming timing = GenerationTiming.INSERT; + GenerationTiming timing = GenerationTiming.NEVER; boolean referenceColumns = false; String[] columnValues = new String[ composite.getColumnSpan() ]; @@ -635,11 +646,28 @@ public class EntityMetamodel implements Serializable { for ( Property property : composite.getProperties() ) { propertyIndex++; final InDatabaseValueGenerationStrategy subStrategy = inDatabaseStrategies.get( propertyIndex ); - - if ( subStrategy.getGenerationTiming() == GenerationTiming.ALWAYS ) { - // override the base-line to the more often "ALWAYS"... - timing = GenerationTiming.ALWAYS; - + switch ( subStrategy.getGenerationTiming() ) { + case INSERT: + switch ( timing ) { + case UPDATE: + timing = GenerationTiming.ALWAYS; + break; + case NEVER: + timing = GenerationTiming.INSERT; + break; + } + break; + case UPDATE: + switch ( timing ) { + case INSERT: + timing = GenerationTiming.ALWAYS; + break; + case NEVER: + timing = GenerationTiming.UPDATE; + } + break; + case ALWAYS: + timing = GenerationTiming.ALWAYS; } if ( subStrategy.referenceColumnsInSql() ) { // override base-line value diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/DatabaseTimestampsColumnTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/DatabaseTimestampsColumnTest.java new file mode 100644 index 0000000000..6e13e70fba --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/DatabaseTimestampsColumnTest.java @@ -0,0 +1,138 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.annotations; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.GenerationTime; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.ValueGenerationType; +import org.hibernate.dialect.Dialect; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.tuple.AnnotationValueGeneration; +import org.hibernate.tuple.GenerationTiming; +import org.hibernate.tuple.ValueGenerator; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Date; + +@Jpa(annotatedClasses = DatabaseTimestampsColumnTest.Person.class) +public class DatabaseTimestampsColumnTest { + + @Entity(name = "Person") + public class Person { + + @Id + @GeneratedValue + private Long id; + + @NaturalId(mutable = true) + private String name; + + @Column(nullable = false) + @Timestamp(GenerationTime.INSERT) + private Date creationDate; + + @Column(nullable = true) + @Timestamp(GenerationTime.UPDATE) + private Date editionDate; + + @Column(nullable = false, name="version") + @Timestamp(GenerationTime.ALWAYS) + private Date timestamp; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getCreationDate() { + return creationDate; + } + + public Date getEditionDate() { + return editionDate; + } + + public Date getTimestamp() { + return timestamp; + } + } + + @ValueGenerationType(generatedBy = TimestampValueGeneration.class) + @Retention(RetentionPolicy.RUNTIME) + public @interface Timestamp { GenerationTime value(); } + + public static class TimestampValueGeneration + implements AnnotationValueGeneration { + + private GenerationTiming timing; + + @Override + public void initialize(Timestamp annotation, Class propertyType) { + timing = annotation.value().getEquivalent(); + } + + public GenerationTiming getGenerationTiming() { + return timing; + } + + public ValueGenerator getValueGenerator() { + return null; + } + + public boolean referenceColumnInSql() { + return true; + } + + public String getDatabaseGeneratedReferencedColumnValue() { + return "current_timestamp"; + } + + public String getDatabaseGeneratedReferencedColumnValue(Dialect dialect) { + return dialect.currentTimestamp(); + } + } + + @Test + public void generatesCurrentTimestamp(EntityManagerFactoryScope scope) { + scope.inEntityManager( + entityManager -> { + entityManager.getTransaction().begin(); + Person person = new Person(); + person.setName("John Doe"); + entityManager.persist(person); + entityManager.getTransaction().commit(); + Date creationDate = person.getCreationDate(); + Assertions.assertNotNull(creationDate); + Assertions.assertNull(person.getEditionDate()); + Date timestamp = person.getTimestamp(); + Assertions.assertNotNull(timestamp); + + try { Thread.sleep(1_000); } catch (InterruptedException ie) {}; + + entityManager.getTransaction().begin(); + person.setName("Jane Doe"); + entityManager.getTransaction().commit(); + Assertions.assertNotNull(person.getCreationDate()); + Assertions.assertEquals(creationDate, person.getCreationDate()); + Assertions.assertNotNull(person.getEditionDate()); + Assertions.assertNotNull(person.getTimestamp()); + Assertions.assertNotEquals(timestamp, person.getTimestamp()); + } + ); + } +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java index 49d56c9042..dbbaad60f4 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/AuditMetadataGenerator.java @@ -123,8 +123,7 @@ public final class AuditMetadataGenerator extends AbstractMetadataGenerator { final ValueGeneration generation = property.getValueGenerationStrategy(); if ( generation instanceof GeneratedValueGeneration ) { final GeneratedValueGeneration valueGeneration = (GeneratedValueGeneration) generation; - if ( GenerationTiming.INSERT == valueGeneration.getGenerationTiming() - || GenerationTiming.ALWAYS == valueGeneration.getGenerationTiming() ) { + if ( valueGeneration.getGenerationTiming().includesInsert() ) { return true; } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 25bead33b6..8922e6de0b 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -366,7 +366,7 @@ abstract public class DialectFeatureChecks { public static class UsesStandardCurrentTimestampFunction implements DialectFeatureCheck { public boolean apply(Dialect dialect) { - return dialect.currentTimestamp().startsWith( "current_timestamp" ); + return dialect.supportsStandardCurrentTimestampFunction(); } }