HHH-15672 introduce Generated(UPDATE) for properties only generated on update

This commit is contained in:
Gavin King 2022-11-04 17:48:54 +01:00
parent aaeed841c8
commit 61c128000b
15 changed files with 297 additions and 56 deletions

View File

@ -314,6 +314,11 @@ public class SybaseLegacyDialect extends AbstractTransactSQLDialect {
throw new UnsupportedOperationException( "format() function not supported on Sybase"); throw new UnsupportedOperationException( "format() function not supported on Sybase");
} }
@Override
public boolean supportsStandardCurrentTimestampFunction() {
return false;
}
@Override @Override
public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData)
throws SQLException { throws SQLException {

View File

@ -25,6 +25,10 @@ public enum GenerationTime {
* Indicates the value is generated on insert. * Indicates the value is generated on insert.
*/ */
INSERT( GenerationTiming.INSERT ), INSERT( GenerationTiming.INSERT ),
/**
* Indicates the value is generated on update.
*/
UPDATE( GenerationTiming.UPDATE ),
/** /**
* Indicates the value is generated on insert and on update. * Indicates the value is generated on insert and on update.
*/ */

View File

@ -998,11 +998,17 @@ public class ModelBinder {
// generated; aka, "insert" is invalid; this is dis-allowed by the DTD, // generated; aka, "insert" is invalid; this is dis-allowed by the DTD,
// but just to make sure... // but just to make sure...
if ( prop.getValueGenerationStrategy() != null ) { if ( prop.getValueGenerationStrategy() != null ) {
if ( prop.getValueGenerationStrategy().getGenerationTiming() == GenerationTiming.INSERT ) { switch ( prop.getValueGenerationStrategy().getGenerationTiming() ) {
case INSERT:
throw new MappingException( throw new MappingException(
"'generated' attribute cannot be 'insert' for version/timestamp property", "'generated' attribute cannot be 'insert' for version/timestamp property",
sourceDocument.getOrigin() 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() ); property.setLazy( singularAttributeSource.isBytecodeLazy() );
final GenerationTiming generationTiming = singularAttributeSource.getGenerationTiming(); final GenerationTiming generationTiming = singularAttributeSource.getGenerationTiming();
if ( generationTiming == GenerationTiming.ALWAYS || generationTiming == GenerationTiming.INSERT ) { if ( generationTiming != null && generationTiming != GenerationTiming.NEVER ) {
// we had generation specified... // we had generation specified...
// HBM only supports "database generated values" // HBM only supports "database generated values"
property.setValueGenerationStrategy( new GeneratedValueGeneration( generationTiming ) ); property.setValueGenerationStrategy( new GeneratedValueGeneration( generationTiming ) );
// generated properties can *never* be insertable... // generated properties can *never* be insertable...
if ( property.isInsertable() ) { if ( property.isInsertable() && generationTiming.includesInsert() ) {
log.debugf( log.debugf(
"Property [%s] specified %s generation, setting insertable to false : %s", "Property [%s] specified %s generation, setting insertable to false : %s",
propertySource.getName(), propertySource.getName(),
@ -2548,7 +2554,7 @@ public class ModelBinder {
} }
// properties generated on update can never be updatable... // properties generated on update can never be updatable...
if ( property.isUpdateable() && generationTiming == GenerationTiming.ALWAYS ) { if ( property.isUpdateable() && generationTiming.includesUpdate() ) {
log.debugf( log.debugf(
"Property [%s] specified ALWAYS generation, setting updateable to false : %s", "Property [%s] specified ALWAYS generation, setting updateable to false : %s",
propertySource.getName(), propertySource.getName(),

View File

@ -434,14 +434,19 @@ public class PropertyBinder {
final Class<? extends AnnotationValueGeneration<?>> generationType = generatorAnnotation.generatedBy(); final Class<? extends AnnotationValueGeneration<?>> generationType = generatorAnnotation.generatedBy();
final AnnotationValueGeneration<A> valueGeneration = instantiateAndInitializeValueGeneration( annotation, generationType, property ); final AnnotationValueGeneration<A> valueGeneration = instantiateAndInitializeValueGeneration( annotation, generationType, property );
if ( annotation.annotationType() == Generated.class if ( annotation.annotationType() == Generated.class && property.isAnnotationPresent(Version.class) ) {
&& property.isAnnotationPresent(Version.class) switch ( valueGeneration.getGenerationTiming() ) {
&& valueGeneration.getGenerationTiming() == GenerationTiming.INSERT ) { case INSERT:
throw new AnnotationException("Property '" + qualify( holder.getPath(), name ) throw new AnnotationException("Property '" + qualify( holder.getPath(), name )
+ "' is annotated '@Generated(INSERT)' and '@Version' (use '@Generated(ALWAYS)' instead)" + "' 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; return valueGeneration;

View File

@ -2142,8 +2142,8 @@ public abstract class Dialect implements ConversionContext {
/** /**
* Should the value returned by {@link #getCurrentTimestampSelectString} * Should the value returned by {@link #getCurrentTimestampSelectString}
* be treated as callable. Typically this indicates that JDBC escape * be treated as callable. Typically, this indicates that JDBC escape
* syntax is being used... * syntax is being used.
* *
* @return True if the {@link #getCurrentTimestampSelectString} return * @return True if the {@link #getCurrentTimestampSelectString} return
* is callable; false otherwise. * 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" ); 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SQLException support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -324,6 +324,11 @@ public class SybaseDialect extends AbstractTransactSQLDialect {
throw new UnsupportedOperationException( "format() function not supported on Sybase"); throw new UnsupportedOperationException( "format() function not supported on Sybase");
} }
@Override
public boolean supportsStandardCurrentTimestampFunction() {
return false;
}
@Override @Override
public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData)
throws SQLException { throws SQLException {

View File

@ -25,27 +25,22 @@ public interface GeneratedValueResolver {
int dbSelectionPosition) { int dbSelectionPosition) {
assert requestedTiming != GenerationTiming.NEVER; assert requestedTiming != GenerationTiming.NEVER;
if ( valueGeneration == null || valueGeneration.getGenerationTiming().includes( GenerationTiming.NEVER ) ) { if ( valueGeneration == null || !valueGeneration.getGenerationTiming().includes( requestedTiming ) ) {
return NoGeneratedValueResolver.INSTANCE; return NoGeneratedValueResolver.INSTANCE;
} }
if ( requestedTiming == GenerationTiming.ALWAYS && valueGeneration.getGenerationTiming() == GenerationTiming.INSERT ) {
return NoGeneratedValueResolver.INSTANCE;
}
// todo (6.x) : incorporate `org.hibernate.tuple.InDatabaseValueGenerationStrategy` // todo (6.x) : incorporate `org.hibernate.tuple.InDatabaseValueGenerationStrategy`
// and `org.hibernate.tuple.InMemoryValueGenerationStrategy` from `EntityMetamodel`. // and `org.hibernate.tuple.InMemoryValueGenerationStrategy` from `EntityMetamodel`.
// this requires unification of the read and write (insert/update) aspects of // 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 // value generation which we'll circle back to as we convert write operations to
// use the "runtime mapping" (`org.hibernate.metamodel.mapping`) model // use the "runtime mapping" (`org.hibernate.metamodel.mapping`) model
else if ( valueGeneration.generatedByDatabase() ) {
if ( valueGeneration.generatedByDatabase() ) {
// in-db generation (column-default, function, etc) // in-db generation (column-default, function, etc)
return new InDatabaseGeneratedValueResolver( requestedTiming, dbSelectionPosition ); return new InDatabaseGeneratedValueResolver( requestedTiming, dbSelectionPosition );
} }
else {
return new InMemoryGeneratedValueResolver( valueGeneration.getValueGenerator(), requestedTiming ); return new InMemoryGeneratedValueResolver( valueGeneration.getValueGenerator(), requestedTiming );
} }
}
GenerationTiming getGenerationTiming(); GenerationTiming getGenerationTiming();
Object resolveGeneratedValue(Object[] row, Object entity, SharedSessionContractImplementor session); Object resolveGeneratedValue(Object[] row, Object entity, SharedSessionContractImplementor session);

View File

@ -66,7 +66,8 @@ public class GeneratedValuesProcessor {
.getEntityMetamodel() .getEntityMetamodel()
.getInDatabaseValueGenerationStrategies(); .getInDatabaseValueGenerationStrategies();
entityDescriptor.visitAttributeMappings( mapping -> { entityDescriptor.visitAttributeMappings( mapping -> {
final InDatabaseValueGenerationStrategy inDatabaseValueGenerationStrategy = inDatabaseValueGenerationStrategies[mapping.getStateArrayPosition()]; final InDatabaseValueGenerationStrategy inDatabaseValueGenerationStrategy =
inDatabaseValueGenerationStrategies[ mapping.getStateArrayPosition() ];
if ( inDatabaseValueGenerationStrategy.getGenerationTiming() == GenerationTiming.NEVER ) { if ( inDatabaseValueGenerationStrategy.getGenerationTiming() == GenerationTiming.NEVER ) {
return; return;
} }

View File

@ -2938,10 +2938,11 @@ public abstract class AbstractEntityPersister
if ( valueGeneration.getGenerationTiming().includesUpdate() if ( valueGeneration.getGenerationTiming().includesUpdate()
&& valueGeneration.generatedByDatabase() && valueGeneration.generatedByDatabase()
&& valueGeneration.referenceColumnInSql() ) { && valueGeneration.referenceColumnInSql() ) {
final Dialect dialect = getFactory().getJdbcServices().getDialect();
update.addColumns( update.addColumns(
getPropertyColumnNames( index ), getPropertyColumnNames( index ),
SINGLE_TRUE, SINGLE_TRUE,
new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue() } new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) }
); );
hasColumns = true; hasColumns = true;
} }
@ -3060,10 +3061,11 @@ public abstract class AbstractEntityPersister
if ( valueGeneration.getGenerationTiming().includesInsert() if ( valueGeneration.getGenerationTiming().includesInsert()
&& valueGeneration.generatedByDatabase() && valueGeneration.generatedByDatabase()
&& valueGeneration.referenceColumnInSql() ) { && valueGeneration.referenceColumnInSql() ) {
final Dialect dialect = getFactory().getJdbcServices().getDialect();
insert.addColumns( insert.addColumns(
getPropertyColumnNames( index ), getPropertyColumnNames( index ),
SINGLE_TRUE, SINGLE_TRUE,
new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue() } new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) }
); );
} }
} }
@ -5766,7 +5768,7 @@ public abstract class AbstractEntityPersister
insertGeneratedValuesProcessor = createGeneratedValuesProcessor( GenerationTiming.INSERT ); insertGeneratedValuesProcessor = createGeneratedValuesProcessor( GenerationTiming.INSERT );
} }
if ( hasUpdateGeneratedProperties() ) { if ( hasUpdateGeneratedProperties() ) {
updateGeneratedValuesProcessor = createGeneratedValuesProcessor( GenerationTiming.ALWAYS ); updateGeneratedValuesProcessor = createGeneratedValuesProcessor( GenerationTiming.UPDATE );
} }
staticFetchableList = new ArrayList<>( attributeMappings.size() ); staticFetchableList = new ArrayList<>( attributeMappings.size() );
visitSubTypeAttributeMappings( attributeMapping -> staticFetchableList.add( attributeMapping ) ); visitSubTypeAttributeMappings( attributeMapping -> staticFetchableList.add( attributeMapping ) );

View File

@ -53,6 +53,25 @@ public enum GenerationTiming {
return timing.includesInsert(); 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. * 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 ) ) { if ( "insert".equalsIgnoreCase( name ) ) {
return INSERT; return INSERT;
} }
else if ( "update".equalsIgnoreCase( name ) ) {
return UPDATE;
}
else if ( "always".equalsIgnoreCase( name ) ) { else if ( "always".equalsIgnoreCase( name ) ) {
return ALWAYS; return ALWAYS;
} }

View File

@ -6,6 +6,8 @@
*/ */
package org.hibernate.tuple; package org.hibernate.tuple;
import org.hibernate.dialect.Dialect;
import java.io.Serializable; import java.io.Serializable;
/** /**
@ -33,6 +35,7 @@ public interface ValueGeneration extends Serializable {
* Specifies that the property value is generated: * Specifies that the property value is generated:
* <ul> * <ul>
* <li>{@linkplain GenerationTiming#INSERT when the entity is inserted}, * <li>{@linkplain GenerationTiming#INSERT when the entity is inserted},
* <li>{@linkplain GenerationTiming#UPDATE when the entity is updated},
* <li>{@linkplain GenerationTiming#ALWAYS whenever the entity is inserted or updated}, or * <li>{@linkplain GenerationTiming#ALWAYS whenever the entity is inserted or updated}, or
* <li>{@linkplain GenerationTiming#NEVER never}. * <li>{@linkplain GenerationTiming#NEVER never}.
* </ul> * </ul>
@ -86,6 +89,27 @@ public interface ValueGeneration extends Serializable {
*/ */
String getDatabaseGeneratedReferencedColumnValue(); 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:
* <ul>
* <li>a function call like {@code current_timestamp} or {@code nextval('mysequence')}, or
* <li>a syntactic marker like {@code default}.
* </ul>
* 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. * Determines if the property value is generated in Java, or by the database.
* <p> * <p>

View File

@ -23,6 +23,7 @@ import org.hibernate.boot.spi.SessionFactoryOptions;
import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper;
import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata;
import org.hibernate.cfg.NotYetImplementedException; import org.hibernate.cfg.NotYetImplementedException;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.OptimisticLockStyle; import org.hibernate.engine.OptimisticLockStyle;
import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadeStyle;
import org.hibernate.engine.spi.CascadeStyles; import org.hibernate.engine.spi.CascadeStyles;
@ -307,24 +308,33 @@ public class EntityMetamodel implements Serializable {
final ValueGenerator<?> generator = pair.getInMemoryStrategy().getValueGenerator(); final ValueGenerator<?> generator = pair.getInMemoryStrategy().getValueGenerator();
if ( generator != null ) { if ( generator != null ) {
// we have some level of generation indicated // we have some level of generation indicated
if ( timing == GenerationTiming.INSERT ) { switch ( timing ) {
case INSERT:
foundPreInsertGeneratedValues = true; foundPreInsertGeneratedValues = true;
} break;
else if ( timing == GenerationTiming.ALWAYS ) { case UPDATE:
foundPreUpdateGeneratedValues = true;
break;
case ALWAYS:
foundPreInsertGeneratedValues = true; foundPreInsertGeneratedValues = true;
foundPreUpdateGeneratedValues = true; foundPreUpdateGeneratedValues = true;
break;
} }
} }
} }
} }
if ( pair.getInDatabaseStrategy() != null ) { if ( pair.getInDatabaseStrategy() != null ) {
final GenerationTiming timing = pair.getInDatabaseStrategy().getGenerationTiming(); switch ( pair.getInDatabaseStrategy().getGenerationTiming() ) {
if ( timing == GenerationTiming.INSERT ) { case INSERT:
foundPostInsertGeneratedValues = true; foundPostInsertGeneratedValues = true;
} break;
else if ( timing == GenerationTiming.ALWAYS ) { case UPDATE:
foundPostUpdateGeneratedValues = true;
break;
case ALWAYS:
foundPostInsertGeneratedValues = true; foundPostInsertGeneratedValues = true;
foundPostUpdateGeneratedValues = true; foundPostUpdateGeneratedValues = true;
break;
} }
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -487,20 +497,21 @@ public class EntityMetamodel implements Serializable {
} }
public static InDatabaseValueGenerationStrategyImpl create( public static InDatabaseValueGenerationStrategyImpl create(
SessionFactoryImplementor sessionFactoryImplementor, SessionFactoryImplementor factory,
Property mappingProperty, Property mappingProperty,
ValueGeneration valueGeneration) { 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 ) { if ( numberOfMappedColumns == 1 ) {
return new InDatabaseValueGenerationStrategyImpl( return new InDatabaseValueGenerationStrategyImpl(
valueGeneration.getGenerationTiming(), valueGeneration.getGenerationTiming(),
valueGeneration.referenceColumnInSql(), valueGeneration.referenceColumnInSql(),
new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue() } new String[] { valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) }
); );
} }
else { else {
if ( valueGeneration.getDatabaseGeneratedReferencedColumnValue() != null ) { if ( valueGeneration.getDatabaseGeneratedReferencedColumnValue(dialect) != null ) {
LOG.debugf( LOG.debugf(
"Value generator specified column value in reference to multi-column attribute [%s -> %s]; ignoring", "Value generator specified column value in reference to multi-column attribute [%s -> %s]; ignoring",
mappingProperty.getPersistentClass(), mappingProperty.getPersistentClass(),
@ -625,7 +636,7 @@ public class EntityMetamodel implements Serializable {
} }
// the base-line values for the aggregated InDatabaseValueGenerationStrategy we will build here. // the base-line values for the aggregated InDatabaseValueGenerationStrategy we will build here.
GenerationTiming timing = GenerationTiming.INSERT; GenerationTiming timing = GenerationTiming.NEVER;
boolean referenceColumns = false; boolean referenceColumns = false;
String[] columnValues = new String[ composite.getColumnSpan() ]; String[] columnValues = new String[ composite.getColumnSpan() ];
@ -635,11 +646,28 @@ public class EntityMetamodel implements Serializable {
for ( Property property : composite.getProperties() ) { for ( Property property : composite.getProperties() ) {
propertyIndex++; propertyIndex++;
final InDatabaseValueGenerationStrategy subStrategy = inDatabaseStrategies.get( propertyIndex ); final InDatabaseValueGenerationStrategy subStrategy = inDatabaseStrategies.get( propertyIndex );
switch ( subStrategy.getGenerationTiming() ) {
if ( subStrategy.getGenerationTiming() == GenerationTiming.ALWAYS ) { case INSERT:
// override the base-line to the more often "ALWAYS"... 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; timing = GenerationTiming.ALWAYS;
} }
if ( subStrategy.referenceColumnsInSql() ) { if ( subStrategy.referenceColumnsInSql() ) {
// override base-line value // override base-line value

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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<Timestamp> {
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());
}
);
}
}

View File

@ -123,8 +123,7 @@ public final class AuditMetadataGenerator extends AbstractMetadataGenerator {
final ValueGeneration generation = property.getValueGenerationStrategy(); final ValueGeneration generation = property.getValueGenerationStrategy();
if ( generation instanceof GeneratedValueGeneration ) { if ( generation instanceof GeneratedValueGeneration ) {
final GeneratedValueGeneration valueGeneration = (GeneratedValueGeneration) generation; final GeneratedValueGeneration valueGeneration = (GeneratedValueGeneration) generation;
if ( GenerationTiming.INSERT == valueGeneration.getGenerationTiming() if ( valueGeneration.getGenerationTiming().includesInsert() ) {
|| GenerationTiming.ALWAYS == valueGeneration.getGenerationTiming() ) {
return true; return true;
} }
} }

View File

@ -366,7 +366,7 @@ abstract public class DialectFeatureChecks {
public static class UsesStandardCurrentTimestampFunction implements DialectFeatureCheck { public static class UsesStandardCurrentTimestampFunction implements DialectFeatureCheck {
public boolean apply(Dialect dialect) { public boolean apply(Dialect dialect) {
return dialect.currentTimestamp().startsWith( "current_timestamp" ); return dialect.supportsStandardCurrentTimestampFunction();
} }
} }