HHH-17322 Generator method that allows state dependent value generation

This commit is contained in:
Marco Belladelli 2023-10-12 09:42:32 +02:00
parent 2bfaa419f3
commit 49eab3fca2
10 changed files with 518 additions and 55 deletions

View File

@ -105,7 +105,7 @@ public abstract class AbstractSaveEventListener<C>
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

View File

@ -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.
* <ul>
* <li>Value generation via arbitrary code written in Java is the responsibility of the method
* {@link BeforeExecutionGenerator#generate(SharedSessionContractImplementor, Object, Object, EventType)}.
@ -34,6 +34,9 @@ import static org.hibernate.generator.EventType.UPDATE;
* SQL {@code select}, though in certain cases this additional round trip may be avoided.
* An important example is id generation using an identity column.
* </ul>
* 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}.
* <p>
* 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.
* <p>
* 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 <b>must</b> 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.

View File

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

View File

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

View File

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

View File

@ -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<Object[]> 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<Object[]> executeSelect(Object id, SharedSessionContractImplementor session) {
final JdbcParameterBindings jdbcParamBindings = getJdbcParameterBindings( id, session );
return session.getFactory().getJdbcServices().getJdbcSelectExecutor()

View File

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

View File

@ -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<ColumnLockingAnalysis> 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"

View File

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

View File

@ -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<EventType> 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++ + "'" };
}
}
}