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");
}
@Override
public boolean supportsStandardCurrentTimestampFunction() {
return false;
}
@Override
public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData)
throws SQLException {

View File

@ -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.
*/

View File

@ -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(),

View File

@ -434,14 +434,19 @@ public class PropertyBinder {
final Class<? extends AnnotationValueGeneration<?>> generationType = generatorAnnotation.generatedBy();
final AnnotationValueGeneration<A> 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;

View File

@ -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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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 {

View File

@ -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();

View File

@ -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;
}

View File

@ -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 ) );

View File

@ -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;
}

View File

@ -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:
* <ul>
* <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#NEVER never}.
* </ul>
@ -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:
* <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.
* <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.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

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();
if ( generation instanceof GeneratedValueGeneration ) {
final GeneratedValueGeneration valueGeneration = (GeneratedValueGeneration) generation;
if ( GenerationTiming.INSERT == valueGeneration.getGenerationTiming()
|| GenerationTiming.ALWAYS == valueGeneration.getGenerationTiming() ) {
if ( valueGeneration.getGenerationTiming().includesInsert() ) {
return true;
}
}

View File

@ -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();
}
}