diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java index 766ffb2bfe..e1106e9af4 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java @@ -105,7 +105,7 @@ public abstract class AbstractSaveEventListener boolean requiresImmediateIdAccess) { final EntityPersister persister = source.getEntityPersister( entityName, entity ); final Generator generator = persister.getGenerator(); - final boolean generatedOnExecution = generator.generatedOnExecution(); + final boolean generatedOnExecution = generator.generatedOnExecution( entity, source ); final Object generatedId; if ( generatedOnExecution ) { // the id gets generated by the database diff --git a/hibernate-core/src/main/java/org/hibernate/generator/Generator.java b/hibernate-core/src/main/java/org/hibernate/generator/Generator.java index 5e8341f2d4..a96c95c2a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/Generator.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/Generator.java @@ -16,10 +16,10 @@ import static org.hibernate.generator.EventType.UPDATE; /** * Describes the generation of values of a certain field or property of an entity. A generated - * value might be generated in Java, or by the database. Every instance must implement either - * {@link BeforeExecutionGenerator} or {@link OnExecutionGenerator} depending on whether values - * are generated in Java code before execution of a SQL statement, or by the database when the - * SQL statement is executed. + * value might be generated in Java, or by the database. Every instance of this interface must + * implement either or both of {@link BeforeExecutionGenerator} and {@link OnExecutionGenerator} + * depending on whether values are generated in Java code before execution of a SQL statement, + * or by the database when the SQL statement is executed. * + * A Generator may implement both interfaces and determine the timing of ID generation at runtime. + * Furthermore, this condition can be based on the state of the owner entity, see + * {@link #generatedOnExecution(Object, SharedSessionContractImplementor) generatedOnExecution}. *

* Generically, a generator may be integrated with the program using the meta-annotation * {@link org.hibernate.annotations.ValueGenerationType}, which associates the generator with @@ -93,6 +96,32 @@ public interface Generator extends Serializable { */ boolean generatedOnExecution(); + + /** + * Determines if the property value is generated when a row is written to the database, + * or in Java code that executes before the row is written. + *

+ * Defaults to {@link #generatedOnExecution()}, but can be overloaded allowing conditional + * value generation timing (on/before execution) based on the current state of the owner entity. + * Note that a generator must implement both {@link BeforeExecutionGenerator} and + * {@link OnExecutionGenerator} to achieve this behavior. + * + * @param entity The instance of the entity owning the attribute for which we are generating a value. + * @param session The session from which the request originates. + * + * @return {@code true} if the value is generated by the database as a side effect of + * the execution of an {@code insert} or {@code update} statement, or false if + * it is generated in Java code before the statement is executed via JDBC. + * + * @see #generatedOnExecution() + * @see BeforeExecutionGenerator + * @see OnExecutionGenerator + * @since 6.4 + */ + default boolean generatedOnExecution(Object entity, SharedSessionContractImplementor session) { + return generatedOnExecution(); + } + /** * The {@linkplain EventType event types} for which this generator should be called * to produce a new value. diff --git a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java index 199692b161..baad1fac7b 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java @@ -10,6 +10,7 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; +import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.OptimizableGenerator; import org.hibernate.id.PersistentIdentifierGenerator; @@ -123,11 +124,18 @@ public class IdentifierGeneratorUtil { ); } - return identifierGeneratorFactory.createIdentifierGenerator( + final Generator generator = identifierGeneratorFactory.createIdentifierGenerator( simpleValue.getIdentifierGeneratorStrategy(), simpleValue.getType(), params ); + + if ( generator.generatedOnExecution() && generator instanceof BeforeExecutionGenerator ) { + // support mixed-timing generators + simpleValue.setNullValue( "undefined" ); + } + + return generator; } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index bc16ca6562..ac5e4b8ba3 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -99,7 +99,7 @@ public class StatelessSessionImpl extends AbstractSharedSessionContract implemen final Object id; final Object[] state = persister.getValues( entity ); final Generator generator = persister.getGenerator(); - if ( !generator.generatedOnExecution() ) { + if ( !generator.generatedOnExecution( entity, this ) ) { id = ( (BeforeExecutionGenerator) generator).generate( this, entity, null, INSERT ); if ( persister.isVersioned() ) { if ( seedVersion( entity, state, persister, this ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java index a7c98a6c50..301d75c36b 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -647,7 +647,7 @@ public class Component extends SimpleValue implements MetaAttributable, Sortable @Override public void execute(SharedSessionContractImplementor session, Object incomingObject, Object injectionContext) { - if ( !subgenerator.generatedOnExecution() ) { + if ( !subgenerator.generatedOnExecution( incomingObject, session ) ) { final Object generatedId = ( (BeforeExecutionGenerator) subgenerator) .generate( session, incomingObject, null, INSERT ); injector.set( injectionContext, generatedId ); 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 216e9cb456..3605976395 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 @@ -111,13 +111,22 @@ public class GeneratedValuesProcessor { * Obtain the generated values, and populate the snapshot and the fields of the entity instance. */ public void processGeneratedValues(Object entity, Object id, Object[] state, SharedSessionContractImplementor session) { - if ( selectStatement != null ) { + if ( selectStatement != null && hasActualGeneratedValuesToSelect( session, entity ) ) { final List results = executeSelect( id, session ); assert results.size() == 1; setEntityAttributes( entity, state, results.get(0) ); } } + private boolean hasActualGeneratedValuesToSelect(SharedSessionContractImplementor session, Object entity) { + for ( AttributeMapping attributeMapping : generatedValuesToSelect ) { + if ( attributeMapping.getGenerator().generatedOnExecution( entity, session ) ) { + return true; + } + } + return false; + } + private List executeSelect(Object id, SharedSessionContractImplementor session) { final JdbcParameterBindings jdbcParamBindings = getJdbcParameterBindings( id, session ); return session.getFactory().getJdbcServices().getJdbcSelectExecutor() diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java index 49fe80b90e..3bf179bb09 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/InsertCoordinator.java @@ -102,32 +102,36 @@ public class InsertCoordinator extends AbstractMutationCoordinator { Object entity, SharedSessionContractImplementor session) { // apply any pre-insert in-memory value generation - preInsertInMemoryValueGeneration( values, entity, session ); + final boolean needsDynamicInsert = preInsertInMemoryValueGeneration( values, entity, session ); final EntityMetamodel entityMetamodel = entityPersister().getEntityMetamodel(); - if ( entityMetamodel.isDynamicInsert() ) { - return doDynamicInserts( id, values, entity, session ); + final boolean forceIdentifierBinding = entityPersister().getGenerator().generatedOnExecution() && id != null; + if ( entityMetamodel.isDynamicInsert() || needsDynamicInsert || forceIdentifierBinding ) { + return doDynamicInserts( id, values, entity, session, forceIdentifierBinding ); } else { return doStaticInserts( id, values, entity, session ); } } - protected void preInsertInMemoryValueGeneration(Object[] values, Object entity, SharedSessionContractImplementor session) { + protected boolean preInsertInMemoryValueGeneration(Object[] values, Object entity, SharedSessionContractImplementor session) { final AbstractEntityPersister persister = entityPersister(); final EntityMetamodel entityMetamodel = persister.getEntityMetamodel(); + boolean foundStateDependentGenerator = false; if ( entityMetamodel.hasPreInsertGeneratedValues() ) { final Generator[] generators = entityMetamodel.getGenerators(); for ( int i = 0; i < generators.length; i++ ) { final Generator generator = generators[i]; if ( generator != null - && !generator.generatedOnExecution() + && !generator.generatedOnExecution( entity, session ) && generator.generatesOnInsert() ) { values[i] = ( (BeforeExecutionGenerator) generator ).generate( session, entity, values[i], INSERT ); persister.setPropertyValue( entity, i, values[i] ); + foundStateDependentGenerator = foundStateDependentGenerator || generator.generatedOnExecution(); } } } + return foundStateDependentGenerator; } protected static class InsertValuesAnalysis implements ValuesAnalysis { @@ -273,9 +277,14 @@ public class InsertCoordinator extends AbstractMutationCoordinator { } } - protected Object doDynamicInserts(Object id, Object[] values, Object object, SharedSessionContractImplementor session) { + protected Object doDynamicInserts( + Object id, + Object[] values, + Object object, + SharedSessionContractImplementor session, + boolean forceIdentifierBinding) { final boolean[] insertability = getPropertiesToInsert( values ); - final MutationOperationGroup insertGroup = generateDynamicInsertSqlGroup( insertability ); + final MutationOperationGroup insertGroup = generateDynamicInsertSqlGroup( insertability, object, session, forceIdentifierBinding ); final MutationExecutor mutationExecutor = executor( session, insertGroup, true ); @@ -330,28 +339,31 @@ public class InsertCoordinator extends AbstractMutationCoordinator { return notNull; } - protected MutationOperationGroup generateDynamicInsertSqlGroup(boolean[] insertable) { - assert entityPersister().getEntityMetamodel().isDynamicInsert(); + protected MutationOperationGroup generateDynamicInsertSqlGroup( + boolean[] insertable, + Object object, + SharedSessionContractImplementor session, + boolean forceIdentifierBinding) { final MutationGroupBuilder insertGroupBuilder = new MutationGroupBuilder( MutationType.INSERT, entityPersister() ); entityPersister().forEachMutableTable( - (tableMapping) -> insertGroupBuilder.addTableDetailsBuilder( createTableInsertBuilder( tableMapping ) ) + (tableMapping) -> insertGroupBuilder.addTableDetailsBuilder( createTableInsertBuilder( tableMapping, forceIdentifierBinding ) ) ); - applyTableInsertDetails( insertGroupBuilder, insertable ); + applyTableInsertDetails( insertGroupBuilder, insertable, object, session, forceIdentifierBinding ); return createOperationGroup( null, insertGroupBuilder.buildMutationGroup() ); } public MutationOperationGroup generateStaticOperationGroup() { final MutationGroupBuilder insertGroupBuilder = new MutationGroupBuilder( MutationType.INSERT, entityPersister() ); entityPersister().forEachMutableTable( - (tableMapping) -> insertGroupBuilder.addTableDetailsBuilder( createTableInsertBuilder( tableMapping ) ) + (tableMapping) -> insertGroupBuilder.addTableDetailsBuilder( createTableInsertBuilder( tableMapping, false ) ) ); - applyTableInsertDetails( insertGroupBuilder, entityPersister().getPropertyInsertability() ); + applyTableInsertDetails( insertGroupBuilder, entityPersister().getPropertyInsertability(), null, null, false ); return createOperationGroup( null, insertGroupBuilder.buildMutationGroup() ); } - private TableInsertBuilder createTableInsertBuilder(EntityTableMapping tableMapping) { + private TableInsertBuilder createTableInsertBuilder(EntityTableMapping tableMapping, boolean forceIdentifierBinding) { final InsertGeneratedIdentifierDelegate identityDelegate = entityPersister().getIdentityInsertDelegate(); - if ( tableMapping.isIdentifierTable() && identityDelegate != null ) { + if ( tableMapping.isIdentifierTable() && identityDelegate != null && !forceIdentifierBinding ) { final BasicEntityIdentifierMapping mapping = (BasicEntityIdentifierMapping) entityPersister().getIdentifierMapping(); return identityDelegate.createTableInsertBuilder( mapping, tableMapping.getInsertExpectation(), factory() ); @@ -363,7 +375,10 @@ public class InsertCoordinator extends AbstractMutationCoordinator { private void applyTableInsertDetails( MutationGroupBuilder insertGroupBuilder, - boolean[] attributeInclusions) { + boolean[] attributeInclusions, + Object object, + SharedSessionContractImplementor session, + boolean forceIdentifierBinding) { final AttributeMappingsList attributeMappings = entityPersister().getAttributeMappings(); insertGroupBuilder.forEachTableMutationBuilder( (builder) -> { @@ -381,8 +396,14 @@ public class InsertCoordinator extends AbstractMutationCoordinator { } else { final Generator generator = attributeMapping.getGenerator(); - if ( isValueGenerationInSql( generator, factory().getJdbcServices().getDialect() ) ) { - handleValueGeneration( attributeMapping, insertGroupBuilder, (OnExecutionGenerator) generator ); + if ( isValueGenerated( generator ) ) { + if ( session != null && !generator.generatedOnExecution( object, session ) ) { + attributeInclusions[attributeIndex] = true; + attributeMapping.forEachInsertable( insertGroupBuilder ); + } + else if ( isValueGenerationInSql( generator, factory().getJdbcServices().getDialect() ) ) { + handleValueGeneration( attributeMapping, insertGroupBuilder, (OnExecutionGenerator) generator ); + } } } } @@ -398,7 +419,7 @@ public class InsertCoordinator extends AbstractMutationCoordinator { final TableInsertBuilder tableInsertBuilder = (TableInsertBuilder) tableMutationBuilder; final EntityTableMapping tableMapping = (EntityTableMapping) tableInsertBuilder.getMutatingTable().getTableMapping(); //noinspection StatementWithEmptyBody - if ( tableMapping.isIdentifierTable() && identityDelegate != null ) { + if ( tableMapping.isIdentifierTable() && identityDelegate != null && !forceIdentifierBinding ) { // nothing to do - the builder already includes the identity handling } else { @@ -407,11 +428,15 @@ public class InsertCoordinator extends AbstractMutationCoordinator { } ); } - private static boolean isValueGenerationInSql(Generator generator, Dialect dialect) { + private static boolean isValueGenerated(Generator generator) { return generator != null - && generator.generatesOnInsert() - && generator.generatedOnExecution() - && ( (OnExecutionGenerator) generator ).referenceColumnsInSql(dialect); + && generator.generatesOnInsert() + && generator.generatedOnExecution(); + } + + private static boolean isValueGenerationInSql(Generator generator, Dialect dialect) { + assert isValueGenerated( generator ); + return ( (OnExecutionGenerator) generator ).referenceColumnsInSql(dialect); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java index 1ce91c4681..576a9b29e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/UpdateCoordinatorStandard.java @@ -288,11 +288,10 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple entityPersister() ); - final InclusionChecker inclusionChecker = - (position, attribute) -> isValueGenerationInSql( attribute.getGenerator(), dialect() ) - || attributeUpdateability[position]; + final InclusionChecker inclusionChecker = (position, attribute) -> attributeUpdateability[position]; final UpdateValuesAnalysisImpl valuesAnalysis = analyzeUpdateValues( + entity, values, oldVersion, incomingOldValues, @@ -446,19 +445,15 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } } - private boolean isValueGenerationInSql(Generator generator, Dialect dialect) { + private static boolean isValueGenerated(Generator generator) { return generator != null - && generator.generatesOnUpdate() - && generator.generatedOnExecution() - && ((OnExecutionGenerator) generator).referenceColumnsInSql(dialect); + && generator.generatesOnUpdate() + && generator.generatedOnExecution(); } - private boolean isValueGenerationInSqlNoWrite(Generator generator, Dialect dialect) { - return generator != null - && generator.generatesOnUpdate() - && generator.generatedOnExecution() - && ((OnExecutionGenerator) generator).referenceColumnsInSql(dialect) - && !((OnExecutionGenerator) generator).writePropertyValue(); + private static boolean isValueGenerationInSql(Generator generator, Dialect dialect) { + assert isValueGenerated( generator ); + return ( (OnExecutionGenerator) generator ).referenceColumnsInSql(dialect); } /** @@ -560,9 +555,9 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple final int[] fieldsPreUpdateNeeded = new int[generators.length]; int count = 0; for ( int i = 0; i < generators.length; i++ ) { - Generator generator = generators[i]; + final Generator generator = generators[i]; if ( generator != null - && !generator.generatedOnExecution() + && !generator.generatedOnExecution( object, session ) && generator.generatesOnUpdate() ) { newValues[i] = ( (BeforeExecutionGenerator) generator ).generate( session, object, newValues[i], UPDATE ); entityPersister().setPropertyValue( object, i, newValues[i] ); @@ -611,6 +606,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } private UpdateValuesAnalysisImpl analyzeUpdateValues( + Object entity, Object[] values, Object oldVersion, Object[] oldValues, @@ -650,6 +646,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple && attributeMapping instanceof SingularAttributeMapping ) { SingularAttributeMapping asSingularAttributeMapping = (SingularAttributeMapping) attributeMapping; processAttribute( + entity, analysis, attributeIndex, asSingularAttributeMapping, @@ -677,6 +674,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } private void processAttribute( + Object entity, UpdateValuesAnalysisImpl analysis, int attributeIndex, SingularAttributeMapping attributeMapping, @@ -686,10 +684,18 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple InclusionChecker lockingChecker, SharedSessionContractImplementor session) { - if ( inclusionChecker.include( attributeIndex, attributeMapping ) ) { + final Generator generator = attributeMapping.getGenerator(); + final boolean generated = isValueGenerated( generator ); + final boolean needsDynamicUpdate = generated && session != null && !generator.generatedOnExecution( entity, session ); + final boolean generatedInSql = generated && isValueGenerationInSql( generator, dialect ); + if ( generatedInSql && !needsDynamicUpdate && !( (OnExecutionGenerator) generator ).writePropertyValue() ) { + analysis.registerValueGeneratedInSqlNoWrite(); + } + + if ( needsDynamicUpdate || generatedInSql || inclusionChecker.include( attributeIndex, attributeMapping ) ) { final int jdbcTypeCount = attributeMapping.getJdbcTypeCount(); for ( int i = 0; i < jdbcTypeCount; i++ ) { - processSet( analysis, attributeMapping.getSelectable( i ) ); + processSet( analysis, attributeMapping.getSelectable( i ), needsDynamicUpdate ); } } @@ -713,10 +719,13 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } } - private void processSet(UpdateValuesAnalysisImpl analysis, SelectableMapping selectable) { + private void processSet(UpdateValuesAnalysisImpl analysis, SelectableMapping selectable, boolean needsDynamicUpdate) { if ( selectable != null && !selectable.isFormula() && selectable.isUpdateable() ) { final EntityTableMapping tableMapping = entityPersister().getPhysicalTableMappingForMutation( selectable ); analysis.registerColumnSet( tableMapping, selectable.getSelectionExpression(), selectable.getWriteExpression() ); + if ( needsDynamicUpdate ) { + analysis.getTablesNeedingDynamicUpdate().add( tableMapping ); + } } } @@ -901,7 +910,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple int attributeIndex, AttributeMapping attributeMapping, IncludedAttributeAnalysis attributeAnalysis) { - if ( isValueGenerationInSqlNoWrite( attributeMapping.getGenerator(), dialect() ) ) { + if ( attributeAnalysis.isValueGeneratedInSqlNoWrite() ) { // we applied `#getDatabaseGeneratedReferencedColumnValue` earlier return false; } @@ -928,6 +937,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple SharedSessionContractImplementor session) { // Create the JDBC operation descriptors final MutationOperationGroup dynamicUpdateGroup = generateDynamicUpdateGroup( + entity, id, rowId, oldValues, @@ -1020,6 +1030,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } protected MutationOperationGroup generateDynamicUpdateGroup( + Object entity, Object id, Object rowId, Object[] oldValues, @@ -1042,6 +1053,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } ); applyTableUpdateDetails( + entity, rowId, updateGroupBuilder, oldValues, @@ -1058,6 +1070,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } private void applyTableUpdateDetails( + Object entity, Object rowId, MutationGroupBuilder updateGroupBuilder, Object[] oldValues, @@ -1082,12 +1095,14 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple if ( attributeAnalysis.includeInSet() ) { assert updateValuesAnalysis.tablesNeedingUpdate.contains( tableMapping ); applyAttributeUpdateDetails( + entity, updateGroupBuilder, dirtinessChecker, versionMapping, attributeIndex, attributeMapping, - (TableUpdateBuilder) builder + (TableUpdateBuilder) builder, + session ); } @@ -1197,14 +1212,18 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } private void applyAttributeUpdateDetails( + Object entity, MutationGroupBuilder updateGroupBuilder, DirtinessChecker dirtinessChecker, EntityVersionMapping versionMapping, int attributeIndex, AttributeMapping attributeMapping, - TableUpdateBuilder tableUpdateBuilder) { + TableUpdateBuilder tableUpdateBuilder, + SharedSessionContractImplementor session) { final Generator generator = attributeMapping.getGenerator(); - if ( isValueGenerationInSql( generator, dialect() ) ) { + if ( isValueGenerated( generator ) + && ( session == null && generator.generatedOnExecution() || generator.generatedOnExecution( entity, session ) ) + && isValueGenerationInSql( generator, dialect ) ) { handleValueGeneration( attributeMapping, updateGroupBuilder, (OnExecutionGenerator) generator ); } else if ( versionMapping != null @@ -1336,6 +1355,10 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple return !tablesNeedingDynamicUpdate.isEmpty(); } + public TableSet getTablesNeedingDynamicUpdate() { + return tablesNeedingDynamicUpdate; + } + /** * Callback at start of processing an attribute */ @@ -1395,6 +1418,11 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple tablesNeedingDynamicUpdate.add( table ); } } + + public void registerValueGeneratedInSqlNoWrite() { + final IncludedAttributeAnalysis attributeAnalysis = (IncludedAttributeAnalysis) currentAttributeAnalysis; + attributeAnalysis.setValueGeneratedInSqlNoWrite( true ); + } } /** @@ -1467,6 +1495,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple private final List columnLockingAnalyses; private DirtynessStatus dirty = DirtynessStatus.NOT_DIRTY; + private boolean valueGeneratedInSqlNoWrite; public IncludedAttributeAnalysis(SingularAttributeMapping attribute) { this.attribute = attribute; @@ -1506,6 +1535,14 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple } } + public boolean isValueGeneratedInSqlNoWrite() { + return valueGeneratedInSqlNoWrite; + } + + public void setValueGeneratedInSqlNoWrite(boolean valueGeneratedInSqlNoWrite) { + this.valueGeneratedInSqlNoWrite = valueGeneratedInSqlNoWrite; + } + @Override public String toString() { return String.format( @@ -1563,7 +1600,8 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple null, null, null, - (index,attribute) -> isValueGenerationInSql( attribute.getGenerator(), dialect() ) + null, + (index,attribute) -> isValueGenerated( attribute.getGenerator() ) && isValueGenerationInSql( attribute.getGenerator(), dialect() ) || entityPersister().getPropertyUpdateability()[index], (index,attribute) -> { switch ( entityPersister().optimisticLockStyle() ) { @@ -1591,6 +1629,7 @@ public class UpdateCoordinatorStandard extends AbstractMutationCoordinator imple // next, iterate each attribute and build the SET and WHERE clauses applyTableUpdateDetails( + null, // row-id "", // pass anything here to generate the row id restriction if possible // the "collector" 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 cd787b7d02..113ea6b3a9 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 @@ -333,6 +333,9 @@ public class EntityMetamodel implements Serializable { if ( generator.generatesOnInsert() ) { if ( generator.generatedOnExecution() ) { foundPostInsertGeneratedValues = true; + if ( generator instanceof BeforeExecutionGenerator ) { + foundPreInsertGeneratedValues = true; + } } else { foundPreInsertGeneratedValues = true; @@ -341,6 +344,9 @@ public class EntityMetamodel implements Serializable { if ( generator.generatesOnUpdate() ) { if ( generator.generatedOnExecution() ) { foundPostUpdateGeneratedValues = true; + if ( generator instanceof BeforeExecutionGenerator ) { + foundPreUpdateGeneratedValues = true; + } } else { foundPreUpdateGeneratedValues = true; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/MixedTimingGeneratorsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/MixedTimingGeneratorsTest.java new file mode 100644 index 0000000000..c8d48667f5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/MixedTimingGeneratorsTest.java @@ -0,0 +1,347 @@ +/* + * 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.idgen.userdefined; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.EnumSet; +import java.util.concurrent.ThreadLocalRandom; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.ValueGenerationType; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.EventType; +import org.hibernate.generator.EventTypeSets; +import org.hibernate.generator.OnExecutionGenerator; +import org.hibernate.id.IdentityGenerator; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + MixedTimingGeneratorsTest.AssignedEntity.class, + MixedTimingGeneratorsTest.RandomEntity.class, + MixedTimingGeneratorsTest.StringGeneratedEntity.class, +} ) +@SessionFactory +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsIdentityColumns.class ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-17322" ) +public class MixedTimingGeneratorsTest { + @Test + @SkipForDialect( dialectClass = SQLServerDialect.class, reason = "SQLServer does not support setting explicit values for identity columns" ) + @SkipForDialect( dialectClass = OracleDialect.class, reason = "Oracle does not support setting explicit values for identity columns" ) + @SkipForDialect( dialectClass = SybaseASEDialect.class, reason = "Sybase does not support setting explicit values for identity columns" ) + public void testIdentityOrAssignedId(SessionFactoryScope scope) { + // on execution generation + scope.inTransaction( session -> session.persist( new AssignedEntity( "identity" ) ) ); + scope.inSession( session -> assertThat( session.createQuery( + "from AssignedEntity where name = :name", + AssignedEntity.class + ).setParameter( "name", "identity" ).getSingleResult().getId() ).isNotEqualTo( 42L ) ); + // before execution generation + scope.inTransaction( session -> session.persist( new AssignedEntity( 42L, "assigned" ) ) ); + scope.inSession( session -> assertThat( session.createQuery( + "from AssignedEntity where name = :name", + AssignedEntity.class + ).setParameter( "name", "assigned" ).getSingleResult().getId() ).isEqualTo( 42L ) ); + } + + @Test + @SkipForDialect( dialectClass = SQLServerDialect.class, reason = "SQLServer does not support setting explicit values for identity columns" ) + @SkipForDialect( dialectClass = OracleDialect.class, reason = "Oracle does not support setting explicit values for identity columns" ) + @SkipForDialect( dialectClass = SybaseASEDialect.class, reason = "Sybase does not support setting explicit values for identity columns" ) + public void testIdentityOrAssignedIdStateless(SessionFactoryScope scope) { + // on execution generation + scope.inStatelessTransaction( session -> session.insert( new AssignedEntity( "stateless_identity" ) ) ); + scope.inStatelessSession( session -> assertThat( session.createQuery( + "from AssignedEntity where name = :name", + AssignedEntity.class + ).setParameter( "name", "stateless_identity" ).getSingleResult().getId() ).isNotEqualTo( 23L ) ); + // before execution generation + scope.inStatelessTransaction( session -> session.insert( new AssignedEntity( 23L, "stateless_assigned" ) ) ); + scope.inStatelessSession( session -> assertThat( session.createQuery( + "from AssignedEntity where name = :name", + AssignedEntity.class + ).setParameter( "name", "stateless_assigned" ).getSingleResult().getId() ).isEqualTo( 23L ) ); + } + + @Test + @SkipForDialect( dialectClass = SQLServerDialect.class, reason = "SQLServer does not support setting explicit values for identity columns" ) + @SkipForDialect( dialectClass = OracleDialect.class, reason = "Oracle does not support setting explicit values for identity columns" ) + @SkipForDialect( dialectClass = SybaseASEDialect.class, reason = "Sybase does not support setting explicit values for identity columns" ) + public void testIdentityOrRandomId(SessionFactoryScope scope) { + // on execution generation + scope.inTransaction( session -> session.persist( new RandomEntity( "identity" ) ) ); + scope.inSession( session -> assertThat( session.createQuery( + "from RandomEntity where name = :name", + RandomEntity.class + ).setParameter( "name", "identity" ).getSingleResult().getId() ).isLessThan( 100L ) ); + // before execution generation + scope.inTransaction( session -> session.persist( new RandomEntity( "random" ) ) ); + scope.inSession( session -> assertThat( session.createQuery( + "from RandomEntity where name = :name", + RandomEntity.class + ).setParameter( "name", "random" ).getSingleResult().getId() ).isGreaterThanOrEqualTo( 100L ) ); + } + + @Test + public void testGeneratedPropInsert(SessionFactoryScope scope) { + // on execution generation + scope.inTransaction( session -> session.persist( new StringGeneratedEntity( 1L, "literal" ) ) ); + scope.inSession( session -> assertThat( + session.find( StringGeneratedEntity.class, 1L ).getGeneratedProp() + ).startsWith( "literal" ) ); + // before execution generation + scope.inTransaction( session -> session.persist( new StringGeneratedEntity( 2L, "generated" ) ) ); + scope.inSession( session -> assertThat( + session.find( StringGeneratedEntity.class, 2L ).getGeneratedProp() + ).startsWith( "generated" ) ); + } + + @Test + public void testGeneratedPropUpdate(SessionFactoryScope scope) { + // on execution generation + final int literalCount = scope.fromTransaction( session -> { + final StringGeneratedEntity entity = new StringGeneratedEntity( 3L, "literal_inserted" ); + session.persist( entity ); + session.flush(); + assertThat( entity.getGeneratedProp() ).startsWith( "literal" ); + entity.setName( "literal_updated" ); + return Integer.parseInt( entity.getGeneratedProp().split( "_" )[1] ); + } ); + scope.inSession( session -> { + final StringGeneratedEntity entity = session.find( StringGeneratedEntity.class, 3L ); + final String generatedProp = entity.getGeneratedProp(); + assertThat( generatedProp ).startsWith( "literal" ); + assertThat( Integer.parseInt( generatedProp.split( "_" )[1] ) ).isGreaterThan( literalCount ); + } ); + // before execution generation + final int generatedCount = scope.fromTransaction( session -> { + final StringGeneratedEntity entity = new StringGeneratedEntity( 4L, "generated_inserted" ); + session.persist( entity ); + session.flush(); + assertThat( entity.getGeneratedProp() ).startsWith( "generated" ); + entity.setName( "generated_updated" ); + return Integer.parseInt( entity.getGeneratedProp().split( "_" )[1] ); + } ); + scope.inSession( session -> { + final StringGeneratedEntity entity = session.find( StringGeneratedEntity.class, 4L ); + final String generatedProp = entity.getGeneratedProp(); + assertThat( generatedProp ).startsWith( "generated" ); + assertThat( Integer.parseInt( generatedProp.split( "_" )[1] ) ).isGreaterThan( generatedCount ); + } ); + } + + @Entity( name = "AssignedEntity" ) + public static class AssignedEntity { + @Id + @GeneratedValue( generator = "identity_or_assigned" ) + @GenericGenerator( name = "identity_or_assigned", type = IdentityOrAssignedGenerator.class ) + private Long id; + + private String name; + + public AssignedEntity() { + } + + public AssignedEntity(String name) { + this.name = name; + } + + public AssignedEntity(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + } + + @Entity( name = "RandomEntity" ) + public static class RandomEntity { + @Id + @GeneratedValue( generator = "identity_or_random" ) + @GenericGenerator( name = "identity_or_random", type = IdentityOrRandomGenerator.class ) + private Long id; + + private String name; + + public RandomEntity() { + } + + public RandomEntity(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + + @ValueGenerationType( generatedBy = LiteralOrGeneratedStringGenerator.class ) + @Retention( RUNTIME ) + @Target( { FIELD, METHOD } ) + public @interface GeneratedString { + /** + * Specifies how the timestamp is generated. By default, it is generated + * in memory, which saves a round trip to the database. + */ + SourceType source() default SourceType.VM; + } + + + @Entity( name = "StringGeneratedEntity" ) + public static class StringGeneratedEntity { + @Id + private Long id; + + private String name; + + @GeneratedString + private String generatedProp; + + public StringGeneratedEntity() { + } + + public StringGeneratedEntity(Long id, String name) { + this.id = id; + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getGeneratedProp() { + return generatedProp; + } + } + + public static class IdentityOrAssignedGenerator extends IdentityGenerator implements BeforeExecutionGenerator { + @Override + public Object generate( + SharedSessionContractImplementor session, + Object owner, + Object currentValue, + EventType eventType) { + final EntityPersister entityPersister = session.getEntityPersister( null, owner ); + return entityPersister.getIdentifier( owner, session ); + } + + @Override + public boolean generatedOnExecution() { + return true; + } + + @Override + public boolean generatedOnExecution(Object owner, SharedSessionContractImplementor session) { + return generate( session, owner, null, null ) == null; + } + } + + public static class IdentityOrRandomGenerator extends IdentityGenerator implements BeforeExecutionGenerator { + @Override + public Object generate( + SharedSessionContractImplementor session, + Object owner, + Object currentValue, + EventType eventType) { + return ThreadLocalRandom.current().nextLong( 100, 1_000 ); + } + + @Override + public boolean generatedOnExecution() { + return true; + } + + @Override + public boolean generatedOnExecution(Object owner, SharedSessionContractImplementor session) { + return !( (RandomEntity) owner ).getName().contains( "random" ); + } + } + + public static class LiteralOrGeneratedStringGenerator implements OnExecutionGenerator, BeforeExecutionGenerator { + private int count; + + public LiteralOrGeneratedStringGenerator() { + count = 0; + } + + @Override + public Object generate( + SharedSessionContractImplementor session, + Object owner, + Object currentValue, + EventType eventType) { + return "generated_" + count++; + } + + @Override + public boolean generatedOnExecution() { + return true; + } + + @Override + public boolean generatedOnExecution(Object owner, SharedSessionContractImplementor session) { + return !( (StringGeneratedEntity) owner ).getName().contains( "generated" ); + } + + @Override + public EnumSet getEventTypes() { + return EventTypeSets.ALL; + } + + @Override + public boolean referenceColumnsInSql(Dialect dialect) { + return true; + } + + @Override + public boolean writePropertyValue() { + return false; + } + + @Override + public String[] getReferencedColumnValues(Dialect dialect) { + return new String[] { "'literal_" + count++ + "'" }; + } + } +}