From 5ecd5c255a6dc34543b18447786db55260994dbf Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Tue, 22 Jul 2025 15:47:21 +0200 Subject: [PATCH 1/7] [#1906] Support @IdGeneratorType This commit : - Deprecate GeneratedValuesMutationDelegateAdaptor because no longger used - Introduce ReactiveGetGeneratedKeysDelegate so avoid adapting the SQL String for Mysql and Oracle - Add support for multi value generation for dialects not supporting returning clause - Deprecate ReactiveConnection insertAndSelectIdentifier and insertAndSelectIdentifierAsResultSet methods in favour of methods supporting multi generated value - Implement ReactiveBasicSelectingDelegate for dealing with Identity columns where the dialect requires an additional command execution to retrieve the generated value - Implement ReaCtiveUniqueKeySelectingDelegate that uses a unique key of the inserted entity to locate the newly inserted row. --- .../adaptor/impl/ResultSetAdaptor.java | 18 +- .../ReactiveEntityIdentityInsertAction.java | 14 ++ .../ReactiveEntityRegularInsertAction.java | 16 +- .../impl/ReactiveEntityUpdateAction.java | 15 +- ...ctiveMutationExecutorSingleNonBatched.java | 12 +- ...eneratedValuesMutationDelegateAdaptor.java | 4 + .../ReactiveGeneratedValuesHelper.java | 93 ++++++----- .../id/ReactiveIdentifierGenerator.java | 22 ++- .../id/ReactiveOnExecutionGenerator.java | 43 +++++ .../ReactiveAbstractReturningDelegate.java | 158 +++++++++--------- .../ReactiveAbstractSelectingDelegate.java | 11 ++ .../ReactiveBasicSelectingDelegate.java | 93 +++++++++++ .../ReactiveGetGeneratedKeysDelegate.java | 66 ++++++++ .../ReactiveInsertReturningDelegate.java | 100 ++++++----- .../ReactiveUniqueKeySelectingDelegate.java | 96 +++++++++++ .../impl/ReactiveIdentityGenerator.java | 37 +++- ...ReactiveJoinedSubclassEntityPersister.java | 11 ++ .../ReactiveSingleTableEntityPersister.java | 5 +- .../ReactiveUnionSubclassEntityPersister.java | 12 ++ .../reactive/pool/BatchingConnection.java | 35 +++- .../reactive/pool/ReactiveConnection.java | 14 ++ .../pool/impl/SqlClientConnection.java | 116 ++++++++----- .../impl/ReactiveStatelessSessionImpl.java | 3 +- 23 files changed, 740 insertions(+), 254 deletions(-) create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveGetGeneratedKeysDelegate.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/ResultSetAdaptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/ResultSetAdaptor.java index b75133699..d3e444c0f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/ResultSetAdaptor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/adaptor/impl/ResultSetAdaptor.java @@ -28,6 +28,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Iterator; @@ -134,8 +135,8 @@ public ResultSetAdaptor(RowSet rows) { this.columnDescriptors = rows.columnDescriptors(); } - public ResultSetAdaptor(RowSet rows, PropertyKind propertyKind, String idColumnName, Class idClass) { - this( rows, rows.property( propertyKind ), idColumnName, idClass ); + public ResultSetAdaptor(RowSet rows, PropertyKind propertyKind, List generatedColumnNames, List> generatedColumnClasses) { + this( rows, rows.property( propertyKind ), generatedColumnNames, generatedColumnClasses ); } public ResultSetAdaptor(RowSet rows, Collection ids, String idColumnName, Class idClass) { @@ -143,11 +144,18 @@ public ResultSetAdaptor(RowSet rows, Collection ids, String idColumnName } private ResultSetAdaptor(RowSet rows, Row row, String idColumnName, Class idClass) { + this( rows, row, List.of( idColumnName ), List.of( idClass ) ); + } + + private ResultSetAdaptor(RowSet rows, Row row, List columnNames, List> columnClasses) { requireNonNull( rows ); - requireNonNull( idColumnName ); + requireNonNull( columnNames ); this.iterator = List.of( row ).iterator(); - this.columnNames = List.of( idColumnName ); - this.columnDescriptors = List.of( toColumnDescriptor( idClass, idColumnName ) ); + this.columnNames = columnNames ; + this.columnDescriptors = new ArrayList<>(columnNames.size()); + for (int i =0; i < columnNames.size(); i++) { + columnDescriptors.add( toColumnDescriptor( columnClasses.get( i ), columnNames.get(i) ) ); + } } private static ColumnDescriptor toColumnDescriptor(Class idClass, String idColumnName) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java index 3dc3a0dc1..4907e46fc 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityIdentityInsertAction.java @@ -16,6 +16,7 @@ import org.hibernate.event.spi.EventSource; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.internal.util.NullnessUtil; +import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; import org.hibernate.stat.spi.StatisticsImplementor; @@ -31,6 +32,7 @@ public class ReactiveEntityIdentityInsertAction extends EntityIdentityInsertActi private final boolean isVersionIncrementDisabled; private boolean executed; private boolean transientReferencesNullified; + private Object rowId; public ReactiveEntityIdentityInsertAction( Object[] state, @@ -108,6 +110,13 @@ private CompletionStage processInsertGeneratedProperties( Object instance, GeneratedValues generatedValues, SharedSessionContractImplementor session) { + final EntityRowIdMapping rowIdMapping = persister.getRowIdMapping(); + if ( rowIdMapping != null ) { + rowId = generatedValues.getGeneratedValue( rowIdMapping ); + if ( rowId != null && !isEarlyInsert() ) { + session.getPersistenceContext().replaceEntityEntryRowId( getInstance(), rowId ); + } + } return persister.hasInsertGeneratedProperties() ? persister.reactiveProcessInsertGenerated( generatedId, instance, getState(), generatedValues, session ) : voidFuture(); @@ -153,4 +162,9 @@ public boolean areTransientReferencesNullified() { public void setTransientReferencesNullified() { transientReferencesNullified = true; } + + @Override + public Object getRowId() { + return rowId; + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java index 977179c73..7f6baac15 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityRegularInsertAction.java @@ -17,6 +17,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; import org.hibernate.stat.spi.StatisticsImplementor; @@ -111,8 +112,19 @@ private CompletionStage processInsertGeneratedProperties( // setVersion( Versioning.getVersion( getState(), persister ) ); } return persister.reactiveProcessInsertGenerated( id, instance, getState(), generatedValues, session ) - .thenAccept( v -> entry.postUpdate( instance, getState(), getVersion() ) ); - + .thenAccept( v -> { + // Process row-id values when available early by replacing the entity entry + if ( generatedValues != null ) { + final EntityRowIdMapping rowIdMapping = persister.getRowIdMapping(); + if ( rowIdMapping != null ) { + final Object rowId = generatedValues.getGeneratedValue( rowIdMapping ); + if ( rowId != null ) { + session.getPersistenceContext().replaceEntityEntryRowId( getInstance(), rowId ); + } + } + } + entry.postUpdate( instance, getState(), getVersion() ); + } ); } else { return voidFuture(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java index 4a6b5e149..817b41683 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java @@ -15,6 +15,7 @@ import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.EventSource; import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.engine.ReactiveExecutable; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; @@ -143,7 +144,19 @@ private CompletionStage processGeneratedProperties( throw new UnsupportedOperationException( "generated version attribute not supported in Hibernate Reactive" ); // setNextVersion( Versioning.getVersion( getState(), persister ) ); } - return persister.reactiveProcessUpdateGenerated( id, instance, getState(), generatedValues, session ); + return persister.reactiveProcessUpdateGenerated( id, instance, getState(), generatedValues, session ) + .thenAccept( v -> { + // Process row-id values when available early by replacing the entity entry + if ( generatedValues != null ) { + final EntityRowIdMapping rowIdMapping = persister.getRowIdMapping(); + if ( rowIdMapping != null ) { + final Object rowId = generatedValues.getGeneratedValue( rowIdMapping ); + if ( rowId != null ) { + session.getPersistenceContext().replaceEntityEntryRowId( getInstance(), rowId ); + } + } + } + } ); } else { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleNonBatched.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleNonBatched.java index 1a37170fc..31de98354 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleNonBatched.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/jdbc/mutation/internal/ReactiveMutationExecutorSingleNonBatched.java @@ -7,8 +7,6 @@ import java.util.concurrent.CompletionStage; -import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.MariaDBDialect; import org.hibernate.engine.jdbc.mutation.OperationResultChecker; import org.hibernate.engine.jdbc.mutation.TableInclusionChecker; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; @@ -48,7 +46,7 @@ public CompletionStage performReactiveNonBatchedOperations( boolean isIdentityInsert, String[] identifierColumnsNames) { PreparedStatementDetails singleStatementDetails = getStatementGroup().getSingleStatementDetails(); - if ( generatedValuesDelegate != null && !isRegularInsertWithMariaDb( session, isIdentityInsert ) ) { + if ( generatedValuesDelegate != null ) { return generatedValuesDelegate.reactivePerformMutation( singleStatementDetails, getJdbcValueBindings(), @@ -67,14 +65,6 @@ public CompletionStage performReactiveNonBatchedOperations( ).thenCompose( CompletionStages::nullFuture ); } - private boolean isRegularInsertWithMariaDb(SharedSessionContractImplementor session, boolean isIdentityInsert) { - if ( isIdentityInsert ) { - return false; - } - Dialect dialect = session.getJdbcServices().getDialect(); - return dialect instanceof MariaDBDialect; - } - @Override public void release() { } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/GeneratedValuesMutationDelegateAdaptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/GeneratedValuesMutationDelegateAdaptor.java index 271d7c5bf..5346eeca2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/GeneratedValuesMutationDelegateAdaptor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/GeneratedValuesMutationDelegateAdaptor.java @@ -19,6 +19,10 @@ import org.hibernate.sql.model.ast.builder.TableMutationBuilder; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; +/** + * @deprecated No longer used + */ +@Deprecated(since = "7.1", forRemoval = true) public class GeneratedValuesMutationDelegateAdaptor implements ReactiveGeneratedValuesMutationDelegate { private final ReactiveGeneratedValuesMutationDelegate delegate; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java index e842adf11..4ff1e15d2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java @@ -7,7 +7,11 @@ import org.hibernate.HibernateException; import org.hibernate.Internal; +import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.OracleDialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; import org.hibernate.generator.values.GeneratedValueBasicResultBuilder; @@ -17,15 +21,16 @@ import org.hibernate.generator.values.internal.GeneratedValuesImpl; import org.hibernate.generator.values.internal.GeneratedValuesMappingProducer; import org.hibernate.id.IdentifierGeneratorHelper; -import org.hibernate.id.insert.GetGeneratedKeysDelegate; -import org.hibernate.id.insert.UniqueKeySelectingDelegate; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.reactive.id.insert.ReactiveGetGeneratedKeysDelegate; import org.hibernate.reactive.id.insert.ReactiveInsertReturningDelegate; +import org.hibernate.reactive.id.insert.ReactiveUniqueKeySelectingDelegate; import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; import org.hibernate.reactive.sql.exec.spi.ReactiveValuesResultSet; import org.hibernate.reactive.sql.results.internal.ReactiveDirectResultSetAccess; @@ -38,7 +43,6 @@ import org.hibernate.sql.results.internal.RowTransformerArrayImpl; import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; -import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; import org.hibernate.type.descriptor.WrapperOptions; import java.sql.PreparedStatement; @@ -51,6 +55,7 @@ import static org.hibernate.generator.values.internal.GeneratedValuesHelper.noCustomSql; import static org.hibernate.internal.NaturalIdHelper.getNaturalIdPropertyNames; import static org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer.UniqueSemantic.NONE; +import static org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions.NO_OPTIONS; /** * @see org.hibernate.generator.values.internal.GeneratedValuesHelper @@ -64,13 +69,21 @@ public class ReactiveGeneratedValuesHelper { * @see GeneratedValuesHelper#getGeneratedValuesDelegate(EntityPersister, EventType) */ public static GeneratedValuesMutationDelegate getGeneratedValuesDelegate(EntityPersister persister, EventType timing) { - final boolean hasGeneratedProperties = !persister.getGeneratedProperties( timing ).isEmpty(); + final List generatedProperties = persister.getGeneratedProperties( timing ); + final boolean hasGeneratedProperties = !generatedProperties.isEmpty(); final boolean hasRowId = timing == EventType.INSERT && persister.getRowIdMapping() != null; final Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); + final boolean hasFormula = + generatedProperties.stream() + .anyMatch( part -> part instanceof SelectableMapping selectable + && selectable.isFormula() ); + + // Cockroach supports insert returning it but the CockroachDb#supportsInsertReturningRowId() wrongly returns false ( https://hibernate.atlassian.net/browse/HHH-19717 ) + boolean supportsInsertReturningRowId = dialect.supportsInsertReturningRowId() || dialect instanceof CockroachDialect; if ( hasRowId - && dialect.supportsInsertReturning() - && dialect.supportsInsertReturningRowId() + && supportsInsertReturning( dialect ) + && supportsInsertReturningRowId && noCustomSql( persister, timing ) ) { // Special case for RowId on INSERT, since GetGeneratedKeysDelegate doesn't support it // make InsertReturningDelegate the preferred method if the dialect supports it @@ -81,26 +94,40 @@ && noCustomSql( persister, timing ) ) { return null; } - if ( dialect.supportsInsertReturningGeneratedKeys() - && persister.getFactory().getSessionFactoryOptions().isGetGeneratedKeysEnabled() ) { - return new GetGeneratedKeysDelegate( persister, false, timing ); - } - else if ( supportsReturning( dialect, timing ) && noCustomSql( persister, timing ) ) { + if ( supportsReturning( dialect, timing ) && noCustomSql( persister, timing ) ) { return new ReactiveInsertReturningDelegate( persister, timing ); } - else if ( timing == EventType.INSERT && persister.getNaturalIdentifierProperties() != null - && !persister.getEntityMetamodel().isNaturalIdentifierInsertGenerated() ) { - return new UniqueKeySelectingDelegate( - persister, - getNaturalIdPropertyNames( persister ), - timing - ); + else if ( !hasFormula && dialect.supportsInsertReturningGeneratedKeys() ) { + return new ReactiveGetGeneratedKeysDelegate( persister, false, timing ); + } + else if ( timing == EventType.INSERT && persister.getNaturalIdentifierProperties() != null && !persister.getEntityMetamodel() + .isNaturalIdentifierInsertGenerated() ) { + return new ReactiveUniqueKeySelectingDelegate( persister, getNaturalIdPropertyNames( persister ), timing ); } return null; } - private static boolean supportsReturning(Dialect dialect, EventType timing) { - return timing == EventType.INSERT ? dialect.supportsInsertReturning() : dialect.supportsUpdateReturning(); + public static boolean supportReactiveGetGeneratedKey(Dialect dialect, List generatedProperties) { + return dialect instanceof OracleDialect + || (dialect instanceof MySQLDialect && generatedProperties.size() == 1 && !(dialect instanceof MariaDBDialect)); + } + + public static boolean supportsReturning(Dialect dialect, EventType timing) { + if ( dialect instanceof CockroachDialect ) { + // Cockroach supports insert and update returning but the CockroachDb#supportsInsertReturning() wrongly returns false ( https://hibernate.atlassian.net/browse/HHH-19717 ) + return true; + } + return timing == EventType.INSERT + ? dialect.supportsInsertReturning() + : dialect.supportsUpdateReturning(); + } + + public static boolean supportsInsertReturning(Dialect dialect) { + if ( dialect instanceof CockroachDialect ) { + // Cockroach supports insert returning but the CockroachDb#supportsInsertReturning() wrongly returns false ( https://hibernate.atlassian.net/browse/HHH-19717 ) + return true; + } + return dialect.supportsInsertReturning(); } /** @@ -181,31 +208,9 @@ private static CompletionStage readGeneratedValues( executionContext ); - final JdbcValuesSourceProcessingOptions processingOptions = new JdbcValuesSourceProcessingOptions() { - @Override - public Object getEffectiveOptionalObject() { - return null; - } - - @Override - public String getEffectiveOptionalEntityName() { - return null; - } - - @Override - public Object getEffectiveOptionalId() { - return null; - } - - @Override - public boolean shouldReturnProxies() { - return true; - } - }; - final JdbcValuesSourceProcessingStateStandardImpl valuesProcessingState = new JdbcValuesSourceProcessingStateStandardImpl( executionContext, - processingOptions + NO_OPTIONS ); final ReactiveRowReader rowReader = ReactiveResultsHelper.createRowReader( @@ -217,7 +222,7 @@ public boolean shouldReturnProxies() { final ReactiveRowProcessingState rowProcessingState = new ReactiveRowProcessingState( valuesProcessingState, executionContext, rowReader, jdbcValues ); return ReactiveListResultsConsumer.instance( NONE ) - .consume( jdbcValues, session, processingOptions, valuesProcessingState, rowProcessingState, rowReader ) + .consume( jdbcValues, session, NO_OPTIONS, valuesProcessingState, rowProcessingState, rowReader ) .thenApply( results -> { if ( results.isEmpty() ) { throw new HibernateException( "The database returned no natively generated values : " + persister.getNavigableRole().getFullPath() ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java index 83ae3290a..3c43b0896 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java @@ -6,11 +6,14 @@ package org.hibernate.reactive.id; import org.hibernate.Incubating; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; -import org.hibernate.generator.Generator; import org.hibernate.id.IdentifierGenerator; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import java.lang.invoke.MethodHandles; import java.util.concurrent.CompletionStage; /** @@ -26,7 +29,8 @@ * @see IdentifierGenerator */ @Incubating -public interface ReactiveIdentifierGenerator extends Generator { +public interface ReactiveIdentifierGenerator extends IdentifierGenerator { + Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); /** * Returns a generated identifier, via a {@link CompletionStage}. @@ -38,4 +42,18 @@ public interface ReactiveIdentifierGenerator extends Generator { default CompletionStage generate(ReactiveConnectionSupplier session, Object owner, Object currentValue, EventType eventType) { return generate( session, owner ); } + + @Override + default Id generate( + SharedSessionContractImplementor session, + Object owner, + Object currentValue, + EventType eventType){ + throw LOG.nonReactiveMethodCall( "generate(ReactiveConnectionSupplier, Object, Object, EventType)" ); + } + + @Override + default Object generate(SharedSessionContractImplementor session, Object object){ + throw LOG.nonReactiveMethodCall( "generate(ReactiveConnectionSupplier, Object)" ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java new file mode 100644 index 000000000..1e28c4cbe --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveOnExecutionGenerator.java @@ -0,0 +1,43 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.id; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.generator.OnExecutionGenerator; +import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.id.insert.ReactiveGetGeneratedKeysDelegate; +import org.hibernate.reactive.id.insert.ReactiveInsertReturningDelegate; +import org.hibernate.reactive.id.insert.ReactiveUniqueKeySelectingDelegate; + +import static org.hibernate.generator.EventType.INSERT; +import static org.hibernate.generator.values.internal.GeneratedValuesHelper.noCustomSql; +import static org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper.supportReactiveGetGeneratedKey; +import static org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper.supportsReturning; + +public interface ReactiveOnExecutionGenerator extends OnExecutionGenerator { + + @Override + default InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(EntityPersister persister) { + final SessionFactoryImplementor factory = persister.getFactory(); + final Dialect dialect = factory.getJdbcServices().getDialect(); + // Hibernate ORM allows the selection of different strategies based on the property `hibernate.jdbc.use_get_generated_keys`. + // But that's a specific JDBC property and with Vert.x we only have one viable option for each supported database. + final boolean supportsInsertReturning = supportsReturning( dialect, INSERT ); + if ( supportsInsertReturning && noCustomSql( persister, INSERT ) ) { + return new ReactiveInsertReturningDelegate( persister, INSERT ); + } + else if ( supportReactiveGetGeneratedKey( dialect, persister.getGeneratedProperties( INSERT ) ) ) { + return new ReactiveGetGeneratedKeysDelegate( persister, false, INSERT ); + } + else { + // let's just hope the entity has a @NaturalId! + return new ReactiveUniqueKeySelectingDelegate( persister, getUniqueKeyPropertyNames( persister ), INSERT ); + } + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java index aed2555fe..31fa7f59e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractReturningDelegate.java @@ -6,63 +6,91 @@ package org.hibernate.reactive.id.insert; import java.lang.invoke.MethodHandles; -import java.sql.PreparedStatement; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletionStage; import org.hibernate.dialect.CockroachDialect; -import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.MySQLDialect; -import org.hibernate.dialect.OracleDialect; -import org.hibernate.dialect.SQLServerDialect; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.EventType; import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.id.insert.AbstractReturningDelegate; import org.hibernate.id.insert.Binder; +import org.hibernate.metamodel.mapping.BasicValuedModelPart; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper; import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.model.ast.MutatingTableReference; import org.hibernate.type.Type; +import static org.hibernate.generator.values.internal.GeneratedValuesHelper.getActualGeneratedModelPart; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; -public interface ReactiveAbstractReturningDelegate extends ReactiveInsertGeneratedIdentifierDelegate { +public abstract class ReactiveAbstractReturningDelegate extends AbstractReturningDelegate implements ReactiveInsertGeneratedIdentifierDelegate { + private static final Log LOG = make( Log.class, MethodHandles.lookup() ); - @Override - PreparedStatement prepareStatement(String insertSql, SharedSessionContractImplementor session); + private final EntityPersister persister; + private final List generatedColumns; + private final List> generatedValueTypes; + + public ReactiveAbstractReturningDelegate( + EntityPersister persister, + EventType timing, + boolean supportsArbitraryValues, + boolean supportsRowId) { + super( persister, timing, supportsArbitraryValues, supportsRowId ); + this.persister = persister; + final var resultBuilders = jdbcValuesMappingProducer.getResultBuilders(); + final MutatingTableReference tableReference = new MutatingTableReference( persister.getIdentifierTableMapping() ); + generatedColumns = new ArrayList<>( resultBuilders.size() ); + this.generatedValueTypes = new ArrayList<>( resultBuilders.size() ); - EntityPersister getPersister(); + for ( var resultBuilder : resultBuilders ) { + final BasicValuedModelPart modelPart = resultBuilder.getModelPart(); + final ColumnReference column = new ColumnReference( tableReference, getActualGeneratedModelPart( modelPart ) ); + generatedColumns.add( column ); + generatedValueTypes.add( modelPart.getJavaType().getJavaTypeClass() ); + } + } @Override - default CompletionStage reactivePerformInsertReturning(String sql, SharedSessionContractImplementor session, Binder binder) { - final String identifierColumnName = getPersister().getIdentifierColumnNames()[0]; - final JdbcServices jdbcServices = session.getJdbcServices(); - final String insertSql = createInsert( sql, identifierColumnName, jdbcServices.getDialect() ); + public CompletionStage reactivePerformInsertReturning(String sql, SharedSessionContractImplementor session, Binder binder) { + final String insertSql = createSqlString( sql, getGeneratedColumnNames(), session.getJdbcServices().getDialect() ); final Object[] params = PreparedStatementAdaptor.bind( binder::bindValues ); - return reactiveExecuteAndExtractReturning( insertSql, params, session ) - .thenApply( this::validateGeneratedIdentityId ); + return reactiveExecuteAndExtractReturning( insertSql, params, session ); } - CompletionStage reactiveExecuteAndExtractReturning(String sql, Object[] params, SharedSessionContractImplementor session); + public CompletionStage reactiveExecuteAndExtractReturning(String sql, Object[] params, SharedSessionContractImplementor session) { + return ( (ReactiveConnectionSupplier) session ) + .getReactiveConnection() + .executeAndSelectGeneratedValues( sql, params, getGeneratedValueTypes(), getGeneratedColumnNames() ) + .thenCompose( rs -> ReactiveGeneratedValuesHelper.getGeneratedValues( rs, persister, getTiming(), session ) ) + .thenApply( this::validateGeneratedIdentityId ); + } @Override - default CompletionStage reactivePerformMutation( + public CompletionStage reactivePerformMutation( PreparedStatementDetails statementDetails, JdbcValueBindings valueBindings, Object entity, SharedSessionContractImplementor session) { - Object[] params = PreparedStatementAdaptor.bind( statement -> { - PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, session.getJdbcServices() ); + final JdbcServices jdbcServices = session.getJdbcServices(); + final Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, jdbcServices ); valueBindings.beforeStatement( details ); } ); - final String identifierColumnName = getPersister().getIdentifierColumnNames()[0]; - final JdbcServices jdbcServices = session.getJdbcServices(); - final String insertSql = createInsert( statementDetails.getSqlString(), identifierColumnName, jdbcServices.getDialect() ); - return reactiveExecuteAndExtractReturning( insertSql, params, session ) - .whenComplete( (generatedValues, throwable) -> { + + final String sql = createSqlString( statementDetails.getSqlString(), getGeneratedColumnNames(), jdbcServices.getDialect() ); + return reactiveExecuteAndExtractReturning( sql, params, session ) + .whenComplete( (values, throwable) -> { if ( statementDetails.getStatement() != null ) { statementDetails.releaseStatement( session ); } @@ -70,69 +98,41 @@ default CompletionStage reactivePerformMutation( } ); } - default GeneratedValues validateGeneratedIdentityId(GeneratedValues generatedId) { - if ( generatedId == null ) { - throw make( Log.class, MethodHandles.lookup() ).noNativelyGeneratedValueReturned(); + public GeneratedValues validateGeneratedIdentityId(GeneratedValues generatedValues) { + if ( generatedValues == null ) { + throw LOG.noNativelyGeneratedValueReturned(); } // CockroachDB might generate an identifier that fits an integer (and maybe a short) from time to time. // Users should not rely on it, or they might have random, hard to debug failures. - Type identifierType = getPersister().getIdentifierType(); + Type identifierType = persister.getIdentifierType(); if ( ( identifierType.getReturnedClass().equals( Short.class ) || identifierType.getReturnedClass().equals( Integer.class ) ) - && getPersister().getFactory().getJdbcServices().getDialect() instanceof CockroachDialect ) { - throw make( Log.class, MethodHandles.lookup() ).invalidIdentifierTypeForCockroachDB( identifierType.getReturnedClass(), getPersister().getEntityName() ); + && persister.getFactory().getJdbcServices().getDialect() instanceof CockroachDialect ) { + throw LOG.invalidIdentifierTypeForCockroachDB( identifierType.getReturnedClass(), persister.getEntityName() ); } - return generatedId; + return generatedValues; } - private static String createInsert(String insertSql, String identifierColumnName, Dialect dialect) { - String sql = insertSql; - final String sqlEnd = " returning " + identifierColumnName; - if ( dialect instanceof MySQLDialect ) { - // For some reason ORM generates a query with an invalid syntax - int index = sql.lastIndexOf( sqlEnd ); - return index > -1 - ? sql.substring( 0, index ) - : sql; - } - if ( dialect instanceof SQLServerDialect ) { - int index = sql.lastIndexOf( sqlEnd ); - // FIXME: this is a hack for HHH-16365 - if ( index > -1 ) { - sql = sql.substring( 0, index ); - } - if ( sql.endsWith( "default values" ) ) { - index = sql.indexOf( "default values" ); - sql = sql.substring( 0, index ); - sql = sql + "output inserted." + identifierColumnName + " default values"; - } - else { - sql = sql.replace( ") values (", ") output inserted." + identifierColumnName + " values (" ); - } - return sql; - } - if ( dialect instanceof DB2Dialect ) { - // ORM query: select id from new table ( insert into IntegerTypeEntity values ( )) - // Correct : select id from new table ( insert into LongTypeEntity (id) values (default)) - return sql.replace( " values ( ))", " (" + identifierColumnName + ") values (default))" ); - } - if ( dialect instanceof OracleDialect ) { - final String valuesStr = " values ( )"; - int index = sql.lastIndexOf( sqlEnd ); - // remove "returning id" since it's added via - if ( index > -1 ) { - sql = sql.substring( 0, index ); - } - - // Oracle is expecting values (default) - if ( sql.endsWith( valuesStr ) ) { - index = sql.lastIndexOf( valuesStr ); - sql = sql.substring( 0, index ); - sql = sql + " values (default)"; - } - - return sql; - } + public String createSqlString(String sql, List columnNames, Dialect dialect){ return sql; } + + private List getGeneratedColumnNames(){ + final List generatedColumns = getGeneratedColumns(); + final List generatedColumnNames = new ArrayList<>(generatedColumns.size()); + generatedColumns.forEach( column -> generatedColumnNames.add( column.getColumnExpression() ) ); + return generatedColumnNames; + } + + protected List getGeneratedColumns(){ + return generatedColumns; + } + + protected List> getGeneratedValueTypes(){ + return generatedValueTypes; + } + + protected EntityPersister getPersister(){ + return persister; + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java new file mode 100644 index 000000000..e6f6dd391 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java @@ -0,0 +1,11 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.id.insert; + +import org.hibernate.reactive.generator.values.ReactiveGeneratedValuesMutationDelegate; + +public interface ReactiveAbstractSelectingDelegate extends ReactiveGeneratedValuesMutationDelegate { +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java new file mode 100644 index 000000000..8c716c338 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java @@ -0,0 +1,93 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.id.insert; + +import org.hibernate.HibernateException; +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.EventType; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.id.insert.AbstractSelectingDelegate; +import org.hibernate.id.insert.BasicSelectingDelegate; +import org.hibernate.jdbc.Expectation; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; +import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; +import org.hibernate.sql.model.ast.builder.TableMutationBuilder; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; + +/** + * @see BasicSelectingDelegate + */ +public class ReactiveBasicSelectingDelegate extends AbstractSelectingDelegate implements ReactiveAbstractSelectingDelegate { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + final private EntityPersister persister; + + public ReactiveBasicSelectingDelegate(EntityPersister persister) { + super( persister, EventType.INSERT, false, false ); + this.persister = persister; + } + + @Override + public CompletionStage reactivePerformMutation( + PreparedStatementDetails statementDetails, + JdbcValueBindings valueBindings, + Object entity, + SharedSessionContractImplementor session) { + final JdbcServices jdbcServices = session.getJdbcServices(); + final Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, jdbcServices ); + valueBindings.beforeStatement( details ); + } ); + return ((ReactiveConnectionSupplier) session).getReactiveConnection() + .update( statementDetails.getSqlString(), params ) + .thenCompose( unused -> getGeneratedValues( session ) ); + } + + private CompletionStage getGeneratedValues(SharedSessionContractImplementor session) { + return ( (ReactiveConnectionSupplier) session ).getReactiveConnection() + .selectJdbc( getSelectSQL() ) + .thenCompose( rs -> ReactiveGeneratedValuesHelper.getGeneratedValues( rs, persister, getTiming(), session ) ); + } + + @Override + public TableMutationBuilder createTableMutationBuilder( Expectation expectation, SessionFactoryImplementor factory) { + return new TableInsertBuilderStandard( persister, persister.getIdentifierTableMapping(), factory ); + } + + @Override + protected String getSelectSQL() { + final String identitySelectString = persister.getIdentitySelectString(); + if ( identitySelectString == null + && !dialect().getIdentityColumnSupport().supportsInsertSelectIdentity() ) { + throw new HibernateException( "Cannot retrieve the generated identity, because the dialect does not support selecting the last generated identity" ); + } + return identitySelectString; + } + + + @Override + public GeneratedValues performMutation( + PreparedStatementDetails singleStatementDetails, + JdbcValueBindings jdbcValueBindings, + Object modelReference, + SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "reactivePerformMutation" ); + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveGetGeneratedKeysDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveGetGeneratedKeysDelegate.java new file mode 100644 index 000000000..8eb1546fc --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveGetGeneratedKeysDelegate.java @@ -0,0 +1,66 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.id.insert; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.EventType; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.jdbc.Expectation; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.sql.model.ast.builder.TableInsertBuilderStandard; +import org.hibernate.sql.model.ast.builder.TableMutationBuilder; +import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; + +import java.lang.invoke.MethodHandles; +import java.sql.PreparedStatement; + +import static java.sql.Statement.NO_GENERATED_KEYS; +import static org.hibernate.generator.EventType.INSERT; + +/** + * ReactiveGetGeneratedKeysDelegate is used for Oracle and MySQL. + * These 2 dbs don't support insert returning but it's still possible to have access + * to these values (for MySQL this is true only for one generated property). + */ +public class ReactiveGetGeneratedKeysDelegate extends ReactiveAbstractReturningDelegate { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveGetGeneratedKeysDelegate( + EntityPersister persister, + boolean inferredKeys, + EventType timing) { + super( persister, timing, !inferredKeys, false ); + } + + @Override + protected GeneratedValues executeAndExtractReturning( + String sql, + PreparedStatement preparedStatement, + SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "reactiveExecuteAndExtractReturning" ); + } + + @Override + public PreparedStatement prepareStatement(String sql, SharedSessionContractImplementor session) { + return session.getJdbcCoordinator().getMutationStatementPreparer().prepareStatement( sql, NO_GENERATED_KEYS ); + } + + @Override + public TableMutationBuilder createTableMutationBuilder( + Expectation expectation, + SessionFactoryImplementor sessionFactory) { + final var identifierTableMapping = getPersister().getIdentifierTableMapping(); + if ( getTiming() == INSERT ) { + return new TableInsertBuilderStandard( getPersister(), identifierTableMapping, sessionFactory ); + } + else { + return new TableUpdateBuilderStandard( getPersister(), identifierTableMapping, sessionFactory ); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java index 90b098652..a3bb5b1cf 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java @@ -7,21 +7,17 @@ import java.lang.invoke.MethodHandles; import java.sql.PreparedStatement; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletionStage; import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.SQLServerDialect; import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; -import org.hibernate.generator.values.GeneratedValueBasicResultBuilder; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.generator.values.internal.TableUpdateReturningBuilder; -import org.hibernate.id.insert.AbstractReturningDelegate; import org.hibernate.id.insert.InsertReturningDelegate; import org.hibernate.id.insert.TableInsertReturningBuilder; import org.hibernate.jdbc.Expectation; @@ -29,40 +25,23 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.reactive.session.ReactiveConnectionSupplier; -import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.model.ast.MutatingTableReference; import org.hibernate.sql.model.ast.builder.TableMutationBuilder; import static java.sql.Statement.NO_GENERATED_KEYS; import static org.hibernate.generator.EventType.INSERT; -import static org.hibernate.generator.values.internal.GeneratedValuesHelper.getActualGeneratedModelPart; -import static org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper.getGeneratedValues; /** * @see InsertReturningDelegate */ -public class ReactiveInsertReturningDelegate extends AbstractReturningDelegate implements ReactiveAbstractReturningDelegate { +public class ReactiveInsertReturningDelegate extends ReactiveAbstractReturningDelegate { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - private final EntityPersister persister; private final MutatingTableReference tableReference; - private final List generatedColumns; public ReactiveInsertReturningDelegate(EntityPersister persister, EventType timing) { - this( persister, timing, false ); - } - - public ReactiveInsertReturningDelegate(EntityPersister persister, Dialect dialect) { - // With JDBC it's possible to enabled GetGeneratedKeys for identity generation. - // Vert.x doesn't have this option, so we always use the same strategy for all database. - // But MySQL requires setting supportsArbitraryValues to false or it's not going to work. - this( persister, INSERT, supportsArbitraryValues( dialect ) ); - } - - private static boolean supportsArbitraryValues( Dialect dialect) { - return !( dialect instanceof MySQLDialect ); + this( persister, timing, true ); } private ReactiveInsertReturningDelegate(EntityPersister persister, EventType timing, boolean supportsArbitraryValues) { @@ -72,33 +51,25 @@ private ReactiveInsertReturningDelegate(EntityPersister persister, EventType tim supportsArbitraryValues, persister.getFactory().getJdbcServices().getDialect().supportsInsertReturningRowId() ); - this.persister = persister; this.tableReference = new MutatingTableReference( persister.getIdentifierTableMapping() ); - final List resultBuilders = jdbcValuesMappingProducer.getResultBuilders(); - this.generatedColumns = new ArrayList<>( resultBuilders.size() ); - for ( GeneratedValueBasicResultBuilder resultBuilder : resultBuilders ) { - generatedColumns.add( new ColumnReference( - tableReference, - getActualGeneratedModelPart( resultBuilder.getModelPart() ) - ) ); - } } + @Override public TableMutationBuilder createTableMutationBuilder( Expectation expectation, SessionFactoryImplementor sessionFactory) { if ( getTiming() == INSERT ) { - return new TableInsertReturningBuilder( persister, tableReference, generatedColumns, sessionFactory ); + return new TableInsertReturningBuilder( getPersister(), tableReference, getGeneratedColumns(), sessionFactory ); } else { - return new TableUpdateReturningBuilder( persister, tableReference, generatedColumns, sessionFactory ); + return new TableUpdateReturningBuilder( getPersister(), tableReference, getGeneratedColumns(), sessionFactory ); } } @Override public String prepareIdentifierGeneratingInsert(String insertSQL) { return dialect().getIdentityColumnSupport().appendIdentitySelectToInsert( - ( (BasicEntityIdentifierMapping) persister.getRootEntityDescriptor().getIdentifierMapping() ).getSelectionExpression(), + ( (BasicEntityIdentifierMapping) getPersister().getRootEntityDescriptor().getIdentifierMapping() ).getSelectionExpression(), insertSQL ); } @@ -108,11 +79,6 @@ public PreparedStatement prepareStatement(String sql, SharedSessionContractImple return session.getJdbcCoordinator().getMutationStatementPreparer().prepareStatement( sql, NO_GENERATED_KEYS ); } - @Override - public EntityPersister getPersister() { - return persister; - } - @Override public GeneratedValues performMutation( PreparedStatementDetails statementDetails, @@ -123,18 +89,50 @@ public GeneratedValues performMutation( } @Override - public CompletionStage reactiveExecuteAndExtractReturning(String sql, Object[] params, SharedSessionContractImplementor session) { - final Class idType = getPersister().getIdentifierType().getReturnedClass(); - final String identifierColumnName = getPersister().getIdentifierColumnNames()[0]; - return ( (ReactiveConnectionSupplier) session ) - .getReactiveConnection() - .insertAndSelectIdentifierAsResultSet( sql, params, idType, identifierColumnName ) - .thenCompose( rs -> getGeneratedValues( rs, getPersister(), getTiming(), session ) ) - .thenApply( this::validateGeneratedIdentityId ); + protected GeneratedValues executeAndExtractReturning(String sql, PreparedStatement preparedStatement, SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "reactiveExecuteAndExtractReturning" ); } @Override - protected GeneratedValues executeAndExtractReturning(String sql, PreparedStatement preparedStatement, SharedSessionContractImplementor session) { - throw LOG.nonReactiveMethodCall( "reactiveExecuteAndExtractReturning" ); + public String createSqlString(String sql, List columnNames, Dialect dialect) { + /* + SQLServerDialect does not fully support insert returning but in Hibernate Reactive + we are using the `output` clause ( it does not work for columns using formulas or having a trigger). + */ + if ( dialect instanceof SQLServerDialect ) { + assert !columnNames.isEmpty(); + final StringBuilder builder = new StringBuilder( sql ); + final int index = builder.lastIndexOf( " returning " + columnNames.get( 0 ) ); + // FIXME: this is a hack for HHH-16365 + if ( index > -1 ) { + builder.delete( index, builder.length() ); + } + final int defaultValues = sql.indexOf( "default values" ); + if ( defaultValues > -1 ) { + builder.delete( defaultValues, builder.length() ); + builder.append( "output " ) + .append( getSQLServerDialectInserted( columnNames ) ) + .append( " default values" ); + } + else { + int start = builder.lastIndexOf( ") values (" ); + builder.replace( + start, + start + 10, + ") output " + getSQLServerDialectInserted( columnNames ) + " values (" + ); + } + return builder.toString(); + } + return sql; + } + + private static String getSQLServerDialectInserted(List generatedColumNames) { + String sql = ""; + for ( String generatedColumName : generatedColumNames ) { + sql += ", inserted." + generatedColumName; + } + // Remove the initial comma + return sql.substring( 2 ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java new file mode 100644 index 000000000..db8b90123 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java @@ -0,0 +1,96 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.id.insert; + +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.EventType; +import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.id.insert.UniqueKeySelectingDelegate; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; +import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.type.Type; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; + +/** + * @see UniqueKeySelectingDelegate + */ +public class ReactiveUniqueKeySelectingDelegate extends UniqueKeySelectingDelegate implements ReactiveAbstractSelectingDelegate { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private final String[] uniqueKeyPropertyNames; + private final Type[] uniqueKeyTypes; + + + public ReactiveUniqueKeySelectingDelegate( + EntityPersister persister, + String[] uniqueKeyPropertyNames, + EventType timing) { + super( persister, uniqueKeyPropertyNames, timing ); + this.uniqueKeyPropertyNames = uniqueKeyPropertyNames; + uniqueKeyTypes = new Type[ uniqueKeyPropertyNames.length ]; + for ( int i = 0; i < uniqueKeyPropertyNames.length; i++ ) { + uniqueKeyTypes[i] = persister.getPropertyType( uniqueKeyPropertyNames[i] ); + } + } + + @Override + public CompletionStage reactivePerformMutation( + PreparedStatementDetails statementDetails, + JdbcValueBindings valueBindings, + Object entity, + SharedSessionContractImplementor session) { + final JdbcServices jdbcServices = session.getJdbcServices(); + final Object[] params = PreparedStatementAdaptor.bind( statement -> { + PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, jdbcServices ); + valueBindings.beforeStatement( details ); + } ); + return ((ReactiveConnectionSupplier) session).getReactiveConnection() + .update( statementDetails.getSqlString(), params ) + .thenCompose( unused -> getGeneratedValues( entity, session ) ); + } + + private CompletionStage getGeneratedValues(Object entity, SharedSessionContractImplementor session) { + return ( (ReactiveConnectionSupplier) session ).getReactiveConnection() + .selectJdbc( getSelectSQL(), getParamValues(entity, session) ) + .thenCompose( rs -> ReactiveGeneratedValuesHelper.getGeneratedValues( rs, persister, getTiming(), session ) ); + } + + protected Object[] getParamValues(Object entity, SharedSessionContractImplementor session) { + return PreparedStatementAdaptor + .bind( statement -> { + int index = 1; + for ( int i = 0; i < uniqueKeyPropertyNames.length; i++ ) { + uniqueKeyTypes[i].nullSafeSet( + statement, + persister.getPropertyValue( entity, uniqueKeyPropertyNames[i] ), + index, + session + ); + index += uniqueKeyTypes[i].getColumnSpan( session.getFactory().getRuntimeMetamodels() ); + } + } ); + } + + @Override + public GeneratedValues performMutation( + PreparedStatementDetails statementDetails, + JdbcValueBindings jdbcValueBindings, + Object entity, + SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "reactivePerformMutation" ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java index d37f07eec..37722fcd1 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java @@ -7,25 +7,46 @@ import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.identity.CockroachDBIdentityColumnSupport; import org.hibernate.id.IdentityGenerator; import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper; +import org.hibernate.reactive.id.insert.ReactiveBasicSelectingDelegate; +import org.hibernate.reactive.id.insert.ReactiveGetGeneratedKeysDelegate; import org.hibernate.reactive.id.insert.ReactiveInsertReturningDelegate; +import org.hibernate.reactive.id.insert.ReactiveUniqueKeySelectingDelegate; + + +import static org.hibernate.generator.EventType.INSERT; +import static org.hibernate.generator.values.internal.GeneratedValuesHelper.noCustomSql; +import static org.hibernate.internal.NaturalIdHelper.getNaturalIdPropertyNames; +import static org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper.supportReactiveGetGeneratedKey; /** * Fix the insert and select id queries generated by Hibernate ORM */ public class ReactiveIdentityGenerator extends IdentityGenerator { - /** - * @see CockroachDBIdentityColumnSupport#supportsIdentityColumns() for some limitations related to CockroachDB - */ @Override public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(EntityPersister persister) { - Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); - // Hibernate ORM allows the selection of different strategies based on the property `hibernate.jdbc.use_get_generated_keys`. - // But that's a specific JDBC property and with Vert.x we only have one viable option for each supported database. - return new ReactiveInsertReturningDelegate( persister, dialect ); + final Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); + /* + Hibernate ORM allows the selection of different strategies based on the property `hibernate.jdbc.use_get_generated_keys` + but Vertex driver does not support get generated keys. + */ + final boolean supportsInsertReturning = ReactiveGeneratedValuesHelper.supportsInsertReturning( dialect ); + if ( supportsInsertReturning && noCustomSql( persister, INSERT ) ) { + return new ReactiveInsertReturningDelegate( persister, INSERT ); + } + else if ( supportReactiveGetGeneratedKey( dialect, persister.getGeneratedProperties( INSERT ) ) ) { + return new ReactiveGetGeneratedKeysDelegate( persister, false, INSERT ); + } + else if ( persister.getNaturalIdentifierProperties() != null + && !persister.getEntityMetamodel().isNaturalIdentifierInsertGenerated() ) { + return new ReactiveUniqueKeySelectingDelegate( persister, getNaturalIdPropertyNames( persister ), INSERT ); + } + else { + return new ReactiveBasicSelectingDelegate( persister ); + } } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java index 2443d4d9b..447369572 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java @@ -19,6 +19,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.Generator; import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.generator.values.GeneratedValuesMutationDelegate; import org.hibernate.loader.ast.spi.MultiIdEntityLoader; import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.loader.ast.spi.SingleIdEntityLoader; @@ -389,4 +390,14 @@ public ReactiveSingleIdArrayLoadPlan reactiveGetSQLLazySelectLoadPlan(String fet return this.getLazyLoadPlanByFetchGroup( getSubclassPropertyNameClosure() ).get(fetchGroup ); } + @Override + public GeneratedValuesMutationDelegate createInsertDelegate() { + return ReactiveAbstractEntityPersister.super.createReactiveInsertDelegate(); + } + + @Override + protected GeneratedValuesMutationDelegate createUpdateDelegate() { + return ReactiveAbstractEntityPersister.super.createReactiveUpdateDelegate(); + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java index 2f5878737..9189b953e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java @@ -42,7 +42,6 @@ import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; import org.hibernate.property.access.spi.PropertyAccess; -import org.hibernate.reactive.generator.values.GeneratedValuesMutationDelegateAdaptor; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; import org.hibernate.reactive.logging.impl.Log; @@ -136,7 +135,7 @@ public GeneratedValuesMutationDelegate getInsertDelegate() { if ( insertDelegate == null ) { return null; } - return new GeneratedValuesMutationDelegateAdaptor( insertDelegate ); + return insertDelegate ; } @Override @@ -145,7 +144,7 @@ public GeneratedValuesMutationDelegate getUpdateDelegate() { if ( updateDelegate == null ) { return null; } - return new GeneratedValuesMutationDelegateAdaptor( updateDelegate ); + return updateDelegate; } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java index bba3cf86d..c280bc35c 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java @@ -21,6 +21,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.Generator; import org.hibernate.generator.values.GeneratedValues; +import org.hibernate.generator.values.GeneratedValuesMutationDelegate; import org.hibernate.id.IdentityGenerator; import org.hibernate.loader.ast.spi.MultiIdEntityLoader; import org.hibernate.loader.ast.spi.MultiIdLoadOptions; @@ -415,4 +416,15 @@ protected ReactiveSingleUniqueKeyEntityLoader getReactiveUniqueKeyLoader public ReactiveSingleIdArrayLoadPlan reactiveGetSQLLazySelectLoadPlan(String fetchGroup) { return this.getLazyLoadPlanByFetchGroup( getSubclassPropertyNameClosure() ).get(fetchGroup ); } + + @Override + public GeneratedValuesMutationDelegate createInsertDelegate() { + return ReactiveAbstractEntityPersister.super.createReactiveInsertDelegate(); + } + + @Override + protected GeneratedValuesMutationDelegate createUpdateDelegate() { + return ReactiveAbstractEntityPersister.super.createReactiveUpdateDelegate(); + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java index ce7ee013d..5e5a96fc9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java @@ -6,12 +6,13 @@ package org.hibernate.reactive.pool; +import org.hibernate.reactive.adaptor.impl.ResultSetAdaptor; + import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletionStage; -import org.hibernate.reactive.adaptor.impl.ResultSetAdaptor; import io.vertx.sqlclient.spi.DatabaseMetadata; @@ -90,6 +91,7 @@ public CompletionStage executeBatch() { } } + @Override public CompletionStage update( String sql, Object[] paramValues, boolean allowBatching, Expectation expectation) { @@ -126,18 +128,22 @@ private boolean hasBatch() { return batchedSql != null; } + @Override public CompletionStage execute(String sql) { return delegate.execute( sql ); } + @Override public CompletionStage executeUnprepared(String sql) { return delegate.executeUnprepared( sql ); } + @Override public CompletionStage executeOutsideTransaction(String sql) { return delegate.executeOutsideTransaction( sql ); } + @Override public CompletionStage update(String sql) { return hasBatch() ? executeBatch().thenCompose( v -> delegate.update( sql ) ) @@ -151,6 +157,7 @@ public CompletionStage update(String sql, Object[] paramValues) { : delegate.update( sql, paramValues ); } + @Override public CompletionStage update(String sql, List paramValues) { return hasBatch() ? executeBatch().thenCompose( v -> delegate.update( sql, paramValues ) ) @@ -173,24 +180,46 @@ public CompletionStage insertAndSelectIdentifierAsResultSet( .thenApply( id -> new ResultSetAdaptor( id, idClass, idColumnName ) ); } + @Override + public CompletionStage executeAndSelectGeneratedValues( + String sql, + Object[] paramValues, + List> idClasses, + List generatedColumnNames) { + return hasBatch() + ? executeBatch().thenCompose( v -> delegate.executeAndSelectGeneratedValues( sql, paramValues, idClasses, generatedColumnNames ) ) + : delegate.executeAndSelectGeneratedValues( sql, paramValues, idClasses, generatedColumnNames ); + } + + @Override public CompletionStage select(String sql) { return hasBatch() ? executeBatch().thenCompose( v -> delegate.select( sql ) ) : delegate.select( sql ); } + @Override public CompletionStage select(String sql, Object[] paramValues) { return hasBatch() ? executeBatch().thenCompose( v -> delegate.select( sql, paramValues ) ) : delegate.select( sql, paramValues ); } + @Override public CompletionStage selectJdbc(String sql, Object[] paramValues) { return hasBatch() ? executeBatch().thenCompose( v -> delegate.selectJdbc( sql, paramValues ) ) : delegate.selectJdbc( sql, paramValues ); } + @Override + public CompletionStage selectJdbc(String sql) { + return hasBatch() + ? executeBatch().thenCompose( v -> delegate.selectJdbc( sql ) ) + : delegate.selectJdbc( sql ); + } + + @Override public CompletionStage selectIdentifier(String sql, Object[] paramValues, Class idClass) { // Do not want to execute the batch here // because we want to be able to select @@ -199,18 +228,22 @@ public CompletionStage selectIdentifier(String sql, Object[] paramValues, return delegate.selectIdentifier( sql, paramValues, idClass ); } + @Override public CompletionStage beginTransaction() { return delegate.beginTransaction(); } + @Override public CompletionStage commitTransaction() { return delegate.commitTransaction(); } + @Override public CompletionStage rollbackTransaction() { return delegate.rollbackTransaction(); } + @Override public CompletionStage close() { return delegate.close(); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java index 10e304116..7cded42e5 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java @@ -60,9 +60,23 @@ interface Expectation { CompletionStage selectJdbc(String sql, Object[] paramValues); + /** + * @deprecated without substitution + */ + @Deprecated CompletionStage insertAndSelectIdentifier(String sql, Object[] paramValues, Class idClass, String idColumnName); + + /** + * @deprecated use {@link #executeAndSelectGeneratedValues(String, Object[], List, List)} + */ + @Deprecated CompletionStage insertAndSelectIdentifierAsResultSet(String sql, Object[] paramValues, Class idClass, String idColumnName); + + CompletionStage selectJdbc(String sql); + + CompletionStage executeAndSelectGeneratedValues(String sql, Object[] paramValues, List> idClass, List generatedColumnName); + CompletionStage selectIdentifier(String sql, Object[] paramValues, Class idClass); interface Result extends Iterator { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java index 3e8647f6c..1f0c33c40 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java @@ -69,6 +69,7 @@ public class SqlClientConnection implements ReactiveConnection { LOG.tracef( "Connection created: %s", connection ); } + @Override public DatabaseMetadata getDatabaseMetadata() { return client().databaseMetadata(); } @@ -128,6 +129,12 @@ public CompletionStage selectJdbc(String sql, Object[] paramValues) { .thenApply( ResultSetAdaptor::new ); } + @Override + public CompletionStage selectJdbc(String sql) { + return preparedQuery( sql ) + .thenApply( ResultSetAdaptor::new ); + } + @Override public CompletionStage execute(String sql) { return preparedQuery( sql ) @@ -213,34 +220,88 @@ public CompletionStage insertAndSelectIdentifierAsResultSet( Object[] parameters, Class idClass, String idColumnName) { + return executeAndSelectGeneratedValues( sql, parameters, List.of( idClass ), List.of( idColumnName ) ); + } + + public CompletionStage insertAndSelectIdentifier(String sql, Tuple parameters, Class idClass, String idColumnName) { // Oracle needs to know the name of the column id in advance, this shouldn't affect the other dbs JsonObject options = new JsonObject() .put( "autoGeneratedKeysIndexes", new JsonArray().add( idColumnName ) ); - translateNulls( parameters ); - return preparedQuery( sql, Tuple.wrap( parameters ), new PrepareOptions( options ) ) + return preparedQuery( sql, parameters, new PrepareOptions( options ) ) .thenApply( rows -> { RowIterator iterator = rows.iterator(); return iterator.hasNext() - ? new ResultSetAdaptor( rows ) - : getLastInsertedIdAsResultSet( rows, idClass, idColumnName ); + ? iterator.next().get( idClass, 0 ) + : getLastInsertedId( rows, idClass, idColumnName ); } ); } - public CompletionStage insertAndSelectIdentifier(String sql, Tuple parameters, Class idClass, String idColumnName) { + @SuppressWarnings("unchecked") + private static T getLastInsertedId(RowSet rows, Class idClass, String idColumnName) { + final Long mySqlId = rows.property( MYSQL_LAST_INSERTED_ID ); + if ( mySqlId != null ) { + if ( Long.class.equals( idClass ) ) { + return (T) mySqlId; + } + if ( Integer.class.equals( idClass ) ) { + return (T) ( Integer.valueOf( mySqlId.intValue() ) ); + } + throw LOG.nativelyGeneratedValueMustBeLong(); + } + final Row oracleKeys = rows.property( ORACLE_GENERATED_KEYS ); + if ( oracleKeys != null ) { + return oracleKeys.get( idClass, idColumnName ); + } + return null; + } + + @Override + public CompletionStage executeAndSelectGeneratedValues( + String sql, + Object[] parameters, + List> generatedValueClasses, + List generatedColumnName) { // Oracle needs to know the name of the column id in advance, this shouldn't affect the other dbs + JsonArray autoGeneratedKeysIndexes = new JsonArray(); + generatedColumnName.forEach( autoGeneratedKeysIndexes::add ); JsonObject options = new JsonObject() - .put( "autoGeneratedKeysIndexes", new JsonArray().add( idColumnName ) ); + .put( "autoGeneratedKeysIndexes", autoGeneratedKeysIndexes ); - return preparedQuery( sql, parameters, new PrepareOptions( options ) ) + translateNulls( parameters ); + return preparedQuery( sql, Tuple.wrap( parameters ), new PrepareOptions( options ) ) .thenApply( rows -> { RowIterator iterator = rows.iterator(); return iterator.hasNext() - ? iterator.next().get( idClass, 0 ) - : getLastInsertedId( rows, idClass, idColumnName ); + ? new ResultSetAdaptor( rows ) + : getLastInsertedGeneratedValuesAsResultSet( rows, generatedColumnName, generatedValueClasses ); } ); } + private ResultSet getLastInsertedGeneratedValuesAsResultSet( + RowSet rows, + List generatedColumnNames, + List> generatedValueClasses) { + final Long mySqlId = rows.property( MYSQL_LAST_INSERTED_ID ); + if ( mySqlId != null ) { + // The MySQL Vert.x driver does not seem to support returning multiple generated values. + final Class idClass = generatedValueClasses.get( 0 ); + final String idColumnName = generatedColumnNames.get( 0 ); + if ( Long.class.equals( idClass ) ) { + return new ResultSetAdaptor( rows, List.of( mySqlId ), idColumnName, Long.class ); + } + if ( Integer.class.equals( idClass ) ) { + return new ResultSetAdaptor( rows, List.of( mySqlId.intValue() ), idColumnName, Integer.class ); + } + throw LOG.nativelyGeneratedValueMustBeLong(); + } + final Row oracleKeys = rows.property( ORACLE_GENERATED_KEYS ); + if ( oracleKeys != null ) { + return new ResultSetAdaptor( rows, ORACLE_GENERATED_KEYS, generatedColumnNames, generatedValueClasses ); + } + return null; + } + public CompletionStage> preparedQuery(String sql, Tuple parameters) { feedback( sql ); return client().preparedQuery( sql ).execute( parameters ).toCompletionStage() @@ -325,43 +386,6 @@ public CompletionStage close() { .toCompletionStage(); } - @SuppressWarnings("unchecked") - private static T getLastInsertedId(RowSet rows, Class idClass, String idColumnName) { - final Long mySqlId = rows.property( MYSQL_LAST_INSERTED_ID ); - if ( mySqlId != null ) { - if ( Long.class.equals( idClass ) ) { - return (T) mySqlId; - } - if ( Integer.class.equals( idClass ) ) { - return (T) ( Integer.valueOf( mySqlId.intValue() ) ); - } - throw LOG.nativelyGeneratedValueMustBeLong(); - } - final Row oracleKeys = rows.property( ORACLE_GENERATED_KEYS ); - if ( oracleKeys != null ) { - return oracleKeys.get( idClass, idColumnName ); - } - return null; - } - - private static ResultSet getLastInsertedIdAsResultSet(RowSet rows, Class idClass, String idColumnName) { - final Long mySqlId = rows.property( MYSQL_LAST_INSERTED_ID ); - if ( mySqlId != null ) { - if ( Long.class.equals( idClass ) ) { - return new ResultSetAdaptor( rows, List.of( mySqlId ), idColumnName, Long.class ); - } - if ( Integer.class.equals( idClass ) ) { - return new ResultSetAdaptor( rows, List.of( mySqlId.intValue() ), idColumnName, Integer.class ); - } - throw LOG.nativelyGeneratedValueMustBeLong(); - } - final Row oracleKeys = rows.property( ORACLE_GENERATED_KEYS ); - if ( oracleKeys != null ) { - return new ResultSetAdaptor( rows, ORACLE_GENERATED_KEYS, idColumnName, idClass ); - } - return null; - } - private void setTransaction(Transaction tx) { transaction = tx; } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java index 5b6d7a135..ae9ba45e3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java @@ -70,6 +70,7 @@ import org.hibernate.reactive.query.sql.spi.ReactiveNativeQueryImplementor; import org.hibernate.reactive.query.sqm.internal.ReactiveSqmQueryImpl; import org.hibernate.reactive.query.sqm.internal.ReactiveSqmSelectionQueryImpl; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; import org.hibernate.reactive.session.ReactiveSqmQueryImplementor; import org.hibernate.reactive.session.ReactiveStatelessSession; import org.hibernate.reactive.util.impl.CompletionStages.Completable; @@ -439,7 +440,7 @@ private CompletionStage generatedIdBeforeInsert( private CompletionStage generateIdForInsert(Object entity, Generator generator, ReactiveEntityPersister persister) { if ( generator instanceof ReactiveIdentifierGenerator reactiveGenerator ) { - return reactiveGenerator.generate( this, this ) + return reactiveGenerator.generate( (ReactiveConnectionSupplier) this, this ) .thenApply( id -> castToIdentifierType( id, persister ) ); } From 1b9f393615726071ec696477ec9aa4e5dac0fafa Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Thu, 17 Jul 2025 11:59:06 +0200 Subject: [PATCH 2/7] [#1906] Test @IdGeneratorType support --- .../BeforeExecutionIdGeneratorTypeTest.java | 156 +++++++ .../MutationDelegateIdentityTest.java | 382 ++++++++++++++++++ ...MutationDelegateJoinedInheritanceTest.java | 298 ++++++++++++++ .../reactive/MutationDelegateTest.java | 308 ++++++++++++++ .../OnExecutionGeneratorTypeTest.java | 124 ++++++ 5 files changed, 1268 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java new file mode 100644 index 000000000..e1ba550c9 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BeforeExecutionIdGeneratorTypeTest.java @@ -0,0 +1,156 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicLong; + +import org.hibernate.annotations.IdGeneratorType; +import org.hibernate.generator.EventType; +import org.hibernate.reactive.id.ReactiveIdentifierGenerator; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; +import org.hibernate.reactive.util.impl.CompletionStages; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) +public class BeforeExecutionIdGeneratorTypeTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Person.class ); + } + + @Test + public void testPersistWithoutTransaction(VertxTestContext context) { + final Person person = new Person( "Janet" ); + // The id should be set by the persist + assertThat( person.getId() ).isNull(); + test( context, getMutinySessionFactory() + // The value won't be persisted on the database, but the id should have been assigned anyway + .withSession( session -> session.persist( person ) ) + .invoke( () -> assertThat( person.getId() ).isGreaterThan( 0 ) ) + // Check that the value has not been saved + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .createNativeQuery( "select * from Person_Table", Tuple.class ).getSingleResultOrNull() ) + ) + .invoke( result -> assertThat( result ).isNull() ) + ); + } + + @Test + public void testPersistWithTransaction(VertxTestContext context) { + final Person person = new Person( "Baldrick" ); + // The id should be set by the persist + assertThat( person.getId() ).isNull(); + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persist( person ) ) + .invoke( () -> assertThat( person.getId() ).isGreaterThan( 0 ) ) + // Check that the value has been saved + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .createQuery( "from Person", Person.class ).getSingleResult() ) + ) + .invoke( p -> { + // The raw type might not be a Long, so we have to cast it + assertThat( p.id ).isEqualTo( person.id ); + assertThat( p.name ).isEqualTo( person.name ); + } ) + + ); + } + + @Entity(name = "Person") + @Table(name = "Person_Table") + public static class Person { + @Id + @SimpleId + Long id; + + String name; + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Person person = (Person) o; + return Objects.equals( name, person.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return id + ":" + name; + } + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @IdGeneratorType(SimpleGenerator.class) + public @interface SimpleId { + } + + public static class SimpleGenerator implements ReactiveIdentifierGenerator { + + private AtomicLong sequence = new AtomicLong( 1 ); + + public SimpleGenerator() { + } + + @Override + public boolean generatedOnExecution() { + return false; + } + + @Override + public EnumSet getEventTypes() { + return EnumSet.of( EventType.INSERT ); + } + + + @Override + public CompletionStage generate(ReactiveConnectionSupplier session, Object entity) { + return CompletionStages.completedFuture( sequence.getAndIncrement() ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java new file mode 100644 index 000000000..63bbe6fba --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java @@ -0,0 +1,382 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.RowId; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.generator.EventType; +import org.hibernate.generator.values.GeneratedValuesMutationDelegate; +import org.hibernate.id.insert.AbstractReturningDelegate; +import org.hibernate.id.insert.AbstractSelectingDelegate; +import org.hibernate.id.insert.UniqueKeySelectingDelegate; +import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.mutiny.impl.MutinySessionImpl; +import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.testing.SqlStatementTracker; +import org.hibernate.sql.model.MutationType; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; + +/** + * Inspired by the test + * {@code org.hibernate.orm.test.mapping.generated.delegate.MutationDelegateIdentityTest} + * in Hibernate ORM. + */ +public class MutationDelegateIdentityTest extends BaseReactiveTest { + + private static SqlStatementTracker sqlTracker; + + @Override + protected Collection> annotatedEntities() { + return List.of( + IdentityOnly.class, + IdentityAndValues.class, + IdentityAndValuesAndRowId.class, + IdentityAndValuesAndRowIdAndNaturalId.class + ); + } + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + // Batch size is only enabled to make sure it's ignored when using mutation delegates + configuration.setProperty( AvailableSettings.STATEMENT_BATCH_SIZE, "5"); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( MutationDelegateIdentityTest::filter, configuration.getProperties() ); + return configuration; + } + + @BeforeEach + public void clearTracker() { + sqlTracker.clear(); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean filter(String s) { + String[] accepted = { "insert ", "update ", "delete ", "select " }; + for ( String valid : accepted ) { + if ( s.toLowerCase().startsWith( valid ) ) { + return true; + } + } + return false; + } + + @Test + public void testInsertGeneratedIdentityOnly(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityOnly.class, MutationType.INSERT ); + + final IdentityOnly entity = new IdentityOnly(); + + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getId() ).isNotNull(); + assertThat( entity.getName() ).isNull(); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertExecutedQueriesCount( delegate instanceof AbstractReturningDelegate ? 1 : 2 ); + } ) + )); + } + + @Test + public void testInsertGeneratedValuesAndIdentity(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityAndValues.class, MutationType.INSERT ); + + final IdentityAndValues entity = new IdentityAndValues(); + + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getId() ).isNotNull(); + assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertExecutedQueriesCount( + delegate instanceof AbstractSelectingDelegate + ? 3 + : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 + ); + }) + )); + } + + @Test + public void testUpdateGeneratedValuesAndIdentity(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityAndValues.class, MutationType.UPDATE ); + final IdentityAndValues entity = new IdentityAndValues(); + + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) ) + .invoke( () -> sqlTracker.clear() ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( IdentityAndValues.class, entity.getId() ) + .invoke( identityAndValues -> identityAndValues.setData( "changed" ) ) + ).chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( IdentityAndValues.class, entity.getId() ) + .invoke( identityAndValues -> { + assertThat( entity.getUpdateDate() ).isNotNull(); + sqlTracker.getLoggedQueries().get( 0 ).startsWith( "select" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertExecutedQueriesCount( + delegate != null && delegate.supportsArbitraryValues() ? 3 : 4 + ); + }) + ) ) ) + ); + } + + @Test + @DisabledFor(value = ORACLE, reason = "Vert.x driver doesn't support RowId type parameters") + public void testInsertGeneratedValuesAndIdentityAndRowId(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityAndValuesAndRowId.class, MutationType.INSERT ); + + final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() + && getDialect().rowId( "" ) != null; + final IdentityAndValuesAndRowId entity = new IdentityAndValuesAndRowId(); + + test(context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getId() ).isNotNull(); + assertThat( entity.getName() ).isEqualTo( "default_name" ); + + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertExecutedQueriesCount( + delegate instanceof AbstractSelectingDelegate + ? 3 + : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 + ); + + if ( shouldHaveRowId ) { + // assert row-id was populated in entity entry + final PersistenceContext pc = ( (MutinySessionImpl) s ).unwrap( ReactiveSession.class ) + .getPersistenceContext(); + final EntityEntry entry = pc.getEntry( entity ); + assertThat( entry.getRowId() ).isNotNull(); + } + sqlTracker.clear(); + entity.setData( "changed" ); + } ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getUpdateDate() ).isNotNull(); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertNumberOfOccurrenceInQueryNoSpace( 0, "id_column", shouldHaveRowId ? 0 : 1 ); + } ) + ).chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( IdentityAndValuesAndRowId.class, entity.getId() ) + .invoke( identityAndValuesAndRowId -> assertThat( identityAndValuesAndRowId.getUpdateDate() ).isNotNull() ) + ) ) + ); + } + + @Test + @DisabledFor(value = ORACLE, reason = "Vert.x driver doesn't support RowId type parameters") + public void testInsertGeneratedValuesAndIdentityAndRowIdAndNaturalId(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( + IdentityAndValuesAndRowIdAndNaturalId.class, + MutationType.INSERT + ); + final IdentityAndValuesAndRowIdAndNaturalId entity = new IdentityAndValuesAndRowIdAndNaturalId( "naturalid_1" ); + + test(context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getId() ).isNotNull(); + assertThat( entity.getName() ).isEqualTo( "default_name" ); + + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + final boolean isUniqueKeyDelegate = delegate instanceof UniqueKeySelectingDelegate; + assertExecutedQueriesCount( + delegate == null || !delegate.supportsArbitraryValues() || isUniqueKeyDelegate ? 2 : 1 + ); + if ( isUniqueKeyDelegate ) { + assertNumberOfOccurrenceInQueryNoSpace( 1, "data", 1 ); + assertNumberOfOccurrenceInQueryNoSpace( 1, "id_column", 1 ); + } + + final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() + && getDialect().rowId( "" ) != null; + if ( shouldHaveRowId ) { + // assert row-id was populated in entity entry + final PersistenceContext pc = ( (MutinySessionImpl) s ).unwrap( ReactiveSession.class ) + .getPersistenceContext(); + final EntityEntry entry = pc.getEntry( entity ); + assertThat( entry.getRowId() ).isNotNull(); + } + } ) ) + ); + } + + private static void assertExecutedQueriesCount(int expected) { + assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expected ); + } + + private static void assertNumberOfOccurrenceInQueryNoSpace(int queryNumber, String toCheck, int expectedNumberOfOccurrences) { + String query = sqlTracker.getLoggedQueries().get( queryNumber ); + int actual = query.split( toCheck, -1 ).length - 1; + assertThat( actual ).as( "number of " + toCheck ).isEqualTo( expectedNumberOfOccurrences ); + } + + private static GeneratedValuesMutationDelegate getDelegate(Class entityClass, MutationType mutationType) { + return ( (SessionFactoryImplementor) factoryManager + .getHibernateSessionFactory() ) + .getMappingMetamodel() + .findEntityDescriptor( entityClass ) + .getMutationDelegate( mutationType ); + } + + @Entity(name = "IdentityOnly") + public static class IdentityOnly { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + + @Entity(name = "IdentityAndValues") + @SuppressWarnings("unused") + public static class IdentityAndValues { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Generated(event = EventType.INSERT) + @ColumnDefault("'default_name'") + private String name; + + @UpdateTimestamp(source = SourceType.DB) + private Date updateDate; + + private String data; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setData(String data) { + this.data = data; + } + } + + @RowId + @Entity(name = "IdentityAndValuesAndRowId") + @SuppressWarnings("unused") + public static class IdentityAndValuesAndRowId { + @Id + @Column(name = "id_column") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Generated(event = EventType.INSERT) + @ColumnDefault("'default_name'") + private String name; + + @UpdateTimestamp(source = SourceType.DB) + private Date updateDate; + + private String data; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setData(String data) { + this.data = data; + } + } + + @RowId + @Entity(name = "IdentityAndValuesAndRowIdAndNaturalId") + @SuppressWarnings("unused") + public static class IdentityAndValuesAndRowIdAndNaturalId { + @Id + @Column(name = "id_column") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Generated(event = EventType.INSERT) + @ColumnDefault("'default_name'") + private String name; + + @NaturalId + private String data; + + public IdentityAndValuesAndRowIdAndNaturalId() { + } + + private IdentityAndValuesAndRowIdAndNaturalId(String data) { + this.data = data; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java new file mode 100644 index 000000000..f84be0b0a --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java @@ -0,0 +1,298 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.generator.EventType; +import org.hibernate.generator.values.GeneratedValuesMutationDelegate; +import org.hibernate.id.insert.AbstractSelectingDelegate; +import org.hibernate.reactive.testing.SqlStatementTracker; +import org.hibernate.sql.model.MutationType; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Inspired by the test + * {@code org.hibernate.orm.test.mapping.generated.delegate.MutationDelegateJoinedInheritanceTest} + * in Hibernate ORM. + */ +public class MutationDelegateJoinedInheritanceTest extends BaseReactiveTest { + private static SqlStatementTracker sqlTracker; + + @Override + protected Collection> annotatedEntities() { + return List.of( BaseEntity.class, ChildEntity.class, NonGeneratedParent.class, GeneratedChild.class ); + } + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( + MutationDelegateJoinedInheritanceTest::filter, + configuration.getProperties() + ); + return configuration; + } + + @BeforeEach + public void clearTracker() { + sqlTracker.clear(); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean filter(String s) { + String[] accepted = { "insert ", "update ", "delete ", "select " }; + for ( String valid : accepted ) { + if ( s.toLowerCase().startsWith( valid ) ) { + return true; + } + } + return false; + } + + @Test + public void testInsertBaseEntity(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( BaseEntity.class, MutationType.INSERT ); + + final BaseEntity entity = new BaseEntity(); + + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getId() ).isNotNull(); + assertThat( entity.getName() ).isEqualTo( "default_name" ); + + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertExecutedQueriesCount( + delegate instanceof AbstractSelectingDelegate + ? 3 + : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 + ); + } ) ) + ); + } + + @Test + public void testInsertChildEntity(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( ChildEntity.class, MutationType.INSERT ); + ChildEntity entity = new ChildEntity(); + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getId() ).isNotNull(); + assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( entity.getChildName() ).isEqualTo( "default_child_name" ); + + if ( delegate instanceof AbstractSelectingDelegate ) { + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).contains( "insert" ); + // Note: this is a current restriction, mutation delegates only retrieve generated values + // on the "root" table, and we expect other values to be read through a subsequent select + assertThat( sqlTracker.getLoggedQueries().get( 3 ) ).startsWith( "select" ); + assertExecutedQueriesCount( 4 ); + } + else { + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).contains( "insert" ); + // Note: this is a current restriction, mutation delegates only retrieve generated values + // on the "root" table, and we expect other values to be read through a subsequent select + assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).startsWith( "select" ); + assertExecutedQueriesCount( 3 ); + } + } ) ) + ); + } + + @Test + public void testUpdateBaseEntity(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( BaseEntity.class, MutationType.UPDATE ); + + final BaseEntity entity = new BaseEntity(); + + test( context, getMutinySessionFactory().withTransaction( s -> s.persist( entity ) ) + .invoke( () -> sqlTracker.clear() ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( BaseEntity.class, entity.getId() ) + .invoke( baseEntity -> baseEntity.setData( "changed" ) ) + .call( s::flush ) + .invoke( baseEntity -> { + assertThat( entity.getUpdateDate() ).isNotNull(); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); + + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertExecutedQueriesCount( + delegate != null && delegate.supportsArbitraryValues() ? 2 : 3 + ); + } ) + ) + ) + ); + } + + @Test + public void testUpdateChildEntity(VertxTestContext context) { + final ChildEntity entity = new ChildEntity(); + test( context, getMutinySessionFactory().withTransaction( s -> s.persist( entity ) ) + .invoke( () -> sqlTracker.clear() ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( ChildEntity.class, entity.getId() ) + .invoke( childEntity -> childEntity.setData( "changed" ) ) + .call( s::flush ) + .invoke( childEntity -> { + assertThat( entity.getUpdateDate() ).isNotNull(); + assertThat( entity.getChildUpdateDate() ).isNotNull(); + + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + // Note: this is a current restriction, mutation delegates only retrieve generated values + // on the "root" table, and we expect other values to be read through a subsequent select + assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).startsWith( "select" ); + + assertExecutedQueriesCount( 3 ); + } ) + ) ) + ); + } + + @Test + public void testGeneratedOnlyOnChild(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( NonGeneratedParent.class, MutationType.UPDATE ); + // Mutation delegates only support generated values on the "root" table + assertThat( delegate ).isNull(); + final GeneratedChild generatedChild = new GeneratedChild(); + generatedChild.setId( 1L ); + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( generatedChild ) + .call( s::flush ) + .invoke( () -> { + assertThat( generatedChild.getName() ).isEqualTo( "child_name" ); + assertExecutedQueriesCount( 3 ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "insert" ); + assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).startsWith( "insert" ); + assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).startsWith( "select" ); + } ) + )); + } + + private static void assertExecutedQueriesCount(int expected) { + assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expected ); + } + + private static GeneratedValuesMutationDelegate getDelegate(Class entityClass, MutationType mutationType) { + GeneratedValuesMutationDelegate mutationDelegate = ( (SessionFactoryImplementor) factoryManager + .getHibernateSessionFactory() ) + .getMappingMetamodel() + .findEntityDescriptor( entityClass ) + .getMutationDelegate( mutationType ); + return mutationDelegate; + } + + @Entity(name = "BaseEntity") + @Inheritance(strategy = InheritanceType.JOINED) + public static class BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Generated(event = EventType.INSERT) + @ColumnDefault("'default_name'") + private String name; + + @UpdateTimestamp(source = SourceType.DB) + private Date updateDate; + + private String data; + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setData(String data) { + this.data = data; + } + } + + @Entity(name = "ChildEntity") + public static class ChildEntity extends BaseEntity { + @Generated(event = EventType.INSERT) + @ColumnDefault("'default_child_name'") + private String childName; + + @UpdateTimestamp(source = SourceType.DB) + private Date childUpdateDate; + + public String getChildName() { + return childName; + } + + public Date getChildUpdateDate() { + return childUpdateDate; + } + } + + @Entity(name = "NonGeneratedParent") + @Inheritance(strategy = InheritanceType.JOINED) + public static class NonGeneratedParent { + @Id + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + } + + @Entity(name = "GeneratedChild") + public static class GeneratedChild extends NonGeneratedParent { + @Generated(event = EventType.INSERT) + @ColumnDefault("'child_name'") + private String name; + + public String getName() { + return name; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java new file mode 100644 index 000000000..9a143214c --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java @@ -0,0 +1,308 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.Generated; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.RowId; +import org.hibernate.annotations.SourceType; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.generator.EventType; +import org.hibernate.generator.values.GeneratedValuesMutationDelegate; +import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.id.insert.ReactiveUniqueKeySelectingDelegate; +import org.hibernate.reactive.mutiny.impl.MutinySessionImpl; +import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.testing.SqlStatementTracker; +import org.hibernate.sql.model.MutationType; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; + +/** + * Inspired by the test + * {@code org.hibernate.orm.test.mapping.generated.delegate.MutationDelegateTest} + * in Hibernate ORM. + */ +public class MutationDelegateTest extends BaseReactiveTest { + + private static SqlStatementTracker sqlTracker; + + @Override + protected Collection> annotatedEntities() { + return List.of( ValuesOnly.class,ValuesAndRowId.class, ValuesAndNaturalId.class ); + } + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + // Batch size is only enabled to make sure it's ignored when using mutation delegates + configuration.setProperty( AvailableSettings.STATEMENT_BATCH_SIZE, "5"); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( MutationDelegateTest::filter, configuration.getProperties() ); + return configuration; + } + + @BeforeEach + public void clearTracker() { + sqlTracker.clear(); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean filter(String s) { + String[] accepted = { "insert ", "update ", "delete ", "select " }; + for ( String valid : accepted ) { + if ( s.toLowerCase().startsWith( valid ) ) { + return true; + } + } + return false; + } + + @Test + public void testInsertGeneratedValues(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesOnly.class, MutationType.INSERT ); + + ValuesOnly entity = new ValuesOnly( 1L ); + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertExecutedQueriesCount( delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 ); + } ) ) + ); + } + + @Test + public void testUpdateGeneratedValues(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesOnly.class, MutationType.UPDATE ); + final ValuesOnly entity = new ValuesOnly( 2L ); + + test( context, getMutinySessionFactory() + .withTransaction( s -> s.persist( entity ) ) + .invoke( () -> sqlTracker.clear() ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( ValuesOnly.class, 2 ) + .invoke( valuesOnly -> valuesOnly.setData( "changed" ) ) + ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( ValuesOnly.class, 2 ) ) + .invoke( valuesOnly -> { + assertThat( entity.getUpdateDate() ).isNotNull(); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertExecutedQueriesCount(delegate != null && delegate.supportsArbitraryValues() ? 3 : 4 ); + } + ) + ) + ); + } + + @Test + @DisabledFor(value = ORACLE, reason = "Vert.x driver doesn't support RowId type parameters") + public void testGeneratedValuesAndRowId(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesAndRowId.class, MutationType.INSERT ); + final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() && getDialect().rowId( "" ) != null; + + final ValuesAndRowId entity = new ValuesAndRowId( 1L ); + test( context, getMutinySessionFactory() + .withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertExecutedQueriesCount(delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 ); + s.getFactory(); + if ( shouldHaveRowId ) { + // assert row-id was populated in entity entry + final PersistenceContext pc = ( (MutinySessionImpl) s ).unwrap( ReactiveSession.class ) + .getPersistenceContext(); + final EntityEntry entry = pc.getEntry( entity ); + assertThat( entry.getRowId() ).isNotNull(); + } + sqlTracker.clear(); + entity.setData( "changed" ); + } ) + .call( s::flush ) + .invoke( () -> { + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertNumberOfOccurrenceInQueryNoSpace( 0, "id_column", shouldHaveRowId ? 0 : 1 ); + } ) + ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( ValuesAndRowId.class, 1 ) + .invoke( valuesAndRowId -> assertThat( valuesAndRowId.getUpdateDate() ).isNotNull() ) + ) ) + ); + } + + @Test + public void testInsertGeneratedValuesAndNaturalId(VertxTestContext context) { + final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesAndNaturalId.class, MutationType.INSERT ); + final ValuesAndNaturalId entity = new ValuesAndNaturalId( 1L, "natural_1" ); + + test( context, getMutinySessionFactory().withTransaction( s -> s + .persist( entity ) + .chain( s::flush ) + .invoke( () -> { + assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + + final boolean isUniqueKeyDelegate = delegate instanceof ReactiveUniqueKeySelectingDelegate; + assertExecutedQueriesCount(delegate == null || isUniqueKeyDelegate ? 2 : 1); + if ( isUniqueKeyDelegate ) { + assertNumberOfOccurrenceInQueryNoSpace( 1, "data", 1 ); + assertNumberOfOccurrenceInQueryNoSpace( 1, "id_column", 0 ); + } + } ) + ) + ); + } + + private static void assertNumberOfOccurrenceInQueryNoSpace(int queryNumber, String toCheck, int expectedNumberOfOccurrences) { + String query = sqlTracker.getLoggedQueries().get( queryNumber ); + int actual = query.split( toCheck, -1 ).length - 1; + assertThat( actual ).as( "number of " + toCheck ).isEqualTo( expectedNumberOfOccurrences ); + } + + private static void assertExecutedQueriesCount(int expected) { + assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expected ); + } + + private static GeneratedValuesMutationDelegate getDelegate(Class entityClass, MutationType mutationType) { + GeneratedValuesMutationDelegate mutationDelegate = ( (SessionFactoryImplementor) factoryManager + .getHibernateSessionFactory() ) + .getMappingMetamodel() + .findEntityDescriptor( entityClass ) + .getMutationDelegate( mutationType ); + return mutationDelegate; + } + + @Entity( name = "ValuesOnly" ) + public static class ValuesOnly { + @Id + private Long id; + + @Generated( event = EventType.INSERT ) + @ColumnDefault( "'default_name'" ) + private String name; + + @UpdateTimestamp( source = SourceType.DB ) + private Date updateDate; + + @SuppressWarnings( "FieldCanBeLocal" ) + private String data; + + public ValuesOnly() { + } + + private ValuesOnly(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setData(String data) { + this.data = data; + } + } + + @RowId + @Entity( name = "ValuesAndRowId" ) + public static class ValuesAndRowId { + @Id + @Column( name = "id_column" ) + private Long id; + + @Generated( event = EventType.INSERT ) + @ColumnDefault( "'default_name'" ) + private String name; + + @UpdateTimestamp( source = SourceType.DB ) + private Date updateDate; + + @SuppressWarnings( "FieldCanBeLocal" ) + private String data; + + public ValuesAndRowId() { + } + + private ValuesAndRowId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public Date getUpdateDate() { + return updateDate; + } + + public void setData(String data) { + this.data = data; + } + } + + @Entity( name = "ValuesAndNaturalId" ) + public static class ValuesAndNaturalId { + @Id + @Column( name = "id_column" ) + private Long id; + + @Generated( event = EventType.INSERT ) + @ColumnDefault( "'default_name'" ) + private String name; + + @NaturalId + private String data; + + public ValuesAndNaturalId() { + } + + private ValuesAndNaturalId(Long id, String data) { + this.id = id; + this.data = data; + } + + public String getName() { + return name; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java new file mode 100644 index 000000000..7ff3ed9d0 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/OnExecutionGeneratorTypeTest.java @@ -0,0 +1,124 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Collection; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; + +import org.hibernate.annotations.IdGeneratorType; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.ValueGenerationType; +import org.hibernate.dialect.Dialect; +import org.hibernate.generator.EventType; +import org.hibernate.generator.EventTypeSets; +import org.hibernate.reactive.id.ReactiveOnExecutionGenerator; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) +public class OnExecutionGeneratorTypeTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Tournament.class ); + } + + @Test + public void testPersist(VertxTestContext context) { + Tournament tournament = new Tournament( "Tekken World Tour" ); + test( + context, getSessionFactory() + .withTransaction( session -> session.persist( tournament ) ) + .thenAccept( v -> { + assertThat( tournament.getId() ).isNotNull(); + assertThat( tournament.getCreated() ).isNotNull(); + } ) + ); + } + + @Entity(name = "Tournament") + public static class Tournament { + @Id + @FunctionCreatedValueId + Date id; + + @NaturalId + String name; + + @FunctionCreatedValue + Date created; + + public Tournament() { + } + + public Tournament(String name) { + this.name = name; + } + + public Date getId() { + return id; + } + + public String getName() { + return name; + } + + public Date getCreated() { + return created; + } + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @IdGeneratorType(FunctionCreationValueGeneration.class) + public @interface FunctionCreatedValueId { + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @ValueGenerationType(generatedBy = FunctionCreationValueGeneration.class) + public @interface FunctionCreatedValue { + } + + public static class FunctionCreationValueGeneration + implements ReactiveOnExecutionGenerator { + + @Override + public boolean referenceColumnsInSql(Dialect dialect) { + return true; + } + + @Override + public boolean writePropertyValue() { + return false; + } + + @Override + public String[] getReferencedColumnValues(Dialect dialect) { + return new String[] { dialect.currentTimestamp() }; + } + + @Override + public EnumSet getEventTypes() { + return EventTypeSets.INSERT_ONLY; + } + } + +} From 7029ebd2dd703c40e2d7019b1c9f6170f86aebda Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Tue, 2 Sep 2025 17:35:51 +0200 Subject: [PATCH 3/7] [#1906] Remove ReactiveAbstractSelectingDelegate It doesn't seem to have any purpose at the moment --- .../id/insert/ReactiveAbstractSelectingDelegate.java | 11 ----------- .../id/insert/ReactiveBasicSelectingDelegate.java | 4 +++- .../id/insert/ReactiveUniqueKeySelectingDelegate.java | 4 +++- 3 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java deleted file mode 100644 index e6f6dd391..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveAbstractSelectingDelegate.java +++ /dev/null @@ -1,11 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.id.insert; - -import org.hibernate.reactive.generator.values.ReactiveGeneratedValuesMutationDelegate; - -public interface ReactiveAbstractSelectingDelegate extends ReactiveGeneratedValuesMutationDelegate { -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java index 8c716c338..612824468 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java @@ -19,6 +19,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.generator.values.ReactiveGeneratedValuesMutationDelegate; import org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; @@ -32,7 +33,8 @@ /** * @see BasicSelectingDelegate */ -public class ReactiveBasicSelectingDelegate extends AbstractSelectingDelegate implements ReactiveAbstractSelectingDelegate { +public class ReactiveBasicSelectingDelegate extends AbstractSelectingDelegate implements + ReactiveGeneratedValuesMutationDelegate { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java index db8b90123..8f0a02b26 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java @@ -15,6 +15,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.generator.values.ReactiveGeneratedValuesMutationDelegate; import org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; @@ -27,7 +28,8 @@ /** * @see UniqueKeySelectingDelegate */ -public class ReactiveUniqueKeySelectingDelegate extends UniqueKeySelectingDelegate implements ReactiveAbstractSelectingDelegate { +public class ReactiveUniqueKeySelectingDelegate extends UniqueKeySelectingDelegate implements + ReactiveGeneratedValuesMutationDelegate { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); From 9e8ca9b9e5e8aff355260674019ed3be8e9b9087 Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Tue, 2 Sep 2025 17:42:16 +0200 Subject: [PATCH 4/7] [#1906] Collect CockroachDB special cases in one method It makes it easier to remove the hack once the issue in Hibernate ORM is solved: https://hibernate.atlassian.net/browse/HHH-19717 --- .../ReactiveGeneratedValuesHelper.java | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java index 4ff1e15d2..680a43f69 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java @@ -79,8 +79,7 @@ public static GeneratedValuesMutationDelegate getGeneratedValuesDelegate(EntityP .anyMatch( part -> part instanceof SelectableMapping selectable && selectable.isFormula() ); - // Cockroach supports insert returning it but the CockroachDb#supportsInsertReturningRowId() wrongly returns false ( https://hibernate.atlassian.net/browse/HHH-19717 ) - boolean supportsInsertReturningRowId = dialect.supportsInsertReturningRowId() || dialect instanceof CockroachDialect; + boolean supportsInsertReturningRowId = trueIfCockroach( dialect, dialect.supportsInsertReturningRowId() ); if ( hasRowId && supportsInsertReturning( dialect ) && supportsInsertReturningRowId @@ -107,27 +106,29 @@ else if ( timing == EventType.INSERT && persister.getNaturalIdentifierProperties return null; } + /** + * Cockroach supports returning SQL for insert and update statements, but the dialect wrongly returns false. + * @see HHH-19717 + */ + private static boolean trueIfCockroach(Dialect dialect, boolean predicate) { + return predicate || dialect instanceof CockroachDialect; + } + public static boolean supportReactiveGetGeneratedKey(Dialect dialect, List generatedProperties) { return dialect instanceof OracleDialect || (dialect instanceof MySQLDialect && generatedProperties.size() == 1 && !(dialect instanceof MariaDBDialect)); } public static boolean supportsReturning(Dialect dialect, EventType timing) { - if ( dialect instanceof CockroachDialect ) { - // Cockroach supports insert and update returning but the CockroachDb#supportsInsertReturning() wrongly returns false ( https://hibernate.atlassian.net/browse/HHH-19717 ) - return true; - } - return timing == EventType.INSERT - ? dialect.supportsInsertReturning() - : dialect.supportsUpdateReturning(); + return trueIfCockroach( + dialect, timing == EventType.INSERT + ? dialect.supportsInsertReturning() + : dialect.supportsUpdateReturning() + ); } public static boolean supportsInsertReturning(Dialect dialect) { - if ( dialect instanceof CockroachDialect ) { - // Cockroach supports insert returning but the CockroachDb#supportsInsertReturning() wrongly returns false ( https://hibernate.atlassian.net/browse/HHH-19717 ) - return true; - } - return dialect.supportsInsertReturning(); + return trueIfCockroach( dialect, dialect.supportsInsertReturning() ); } /** From 0a23de34b47eb5e00ca6fd03ee6eea0eb1021ba1 Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Tue, 2 Sep 2025 17:42:34 +0200 Subject: [PATCH 5/7] [#1906] Minor refactoring and clean up --- .../internal/ReactiveGeneratedValuesHelper.java | 15 ++++++++------- .../id/insert/ReactiveBasicSelectingDelegate.java | 5 ++--- .../ReactiveUniqueKeySelectingDelegate.java | 5 ++--- .../entity/impl/ReactiveIdentityGenerator.java | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java index 680a43f69..348a01414 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java @@ -65,7 +65,6 @@ public class ReactiveGeneratedValuesHelper { private static final CoreMessageLogger LOG = CoreLogging.messageLogger( IdentifierGeneratorHelper.class ); /** - * * @see GeneratedValuesHelper#getGeneratedValuesDelegate(EntityPersister, EventType) */ public static GeneratedValuesMutationDelegate getGeneratedValuesDelegate(EntityPersister persister, EventType timing) { @@ -74,10 +73,8 @@ public static GeneratedValuesMutationDelegate getGeneratedValuesDelegate(EntityP final boolean hasRowId = timing == EventType.INSERT && persister.getRowIdMapping() != null; final Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); - final boolean hasFormula = - generatedProperties.stream() - .anyMatch( part -> part instanceof SelectableMapping selectable - && selectable.isFormula() ); + final boolean hasFormula = generatedProperties.stream() + .anyMatch( ReactiveGeneratedValuesHelper::isFormula ); boolean supportsInsertReturningRowId = trueIfCockroach( dialect, dialect.supportsInsertReturningRowId() ); if ( hasRowId @@ -99,8 +96,8 @@ && noCustomSql( persister, timing ) ) { else if ( !hasFormula && dialect.supportsInsertReturningGeneratedKeys() ) { return new ReactiveGetGeneratedKeysDelegate( persister, false, timing ); } - else if ( timing == EventType.INSERT && persister.getNaturalIdentifierProperties() != null && !persister.getEntityMetamodel() - .isNaturalIdentifierInsertGenerated() ) { + else if ( timing == EventType.INSERT && persister.getNaturalIdentifierProperties() != null + && !persister.getEntityMetamodel().isNaturalIdentifierInsertGenerated() ) { return new ReactiveUniqueKeySelectingDelegate( persister, getNaturalIdPropertyNames( persister ), timing ); } return null; @@ -114,6 +111,10 @@ private static boolean trueIfCockroach(Dialect dialect, boolean predicate) { return predicate || dialect instanceof CockroachDialect; } + private static boolean isFormula(ModelPart part) { + return part instanceof SelectableMapping selectable && selectable.isFormula(); + } + public static boolean supportReactiveGetGeneratedKey(Dialect dialect, List generatedProperties) { return dialect instanceof OracleDialect || (dialect instanceof MySQLDialect && generatedProperties.size() == 1 && !(dialect instanceof MariaDBDialect)); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java index 612824468..5c2abedee 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveBasicSelectingDelegate.java @@ -14,7 +14,6 @@ import org.hibernate.generator.EventType; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.id.insert.AbstractSelectingDelegate; -import org.hibernate.id.insert.BasicSelectingDelegate; import org.hibernate.jdbc.Expectation; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; @@ -31,7 +30,7 @@ import java.util.concurrent.CompletionStage; /** - * @see BasicSelectingDelegate + * @see org.hibernate.id.insert.BasicSelectingDelegate */ public class ReactiveBasicSelectingDelegate extends AbstractSelectingDelegate implements ReactiveGeneratedValuesMutationDelegate { @@ -56,7 +55,7 @@ public CompletionStage reactivePerformMutation( PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, jdbcServices ); valueBindings.beforeStatement( details ); } ); - return ((ReactiveConnectionSupplier) session).getReactiveConnection() + return ( (ReactiveConnectionSupplier) session ).getReactiveConnection() .update( statementDetails.getSqlString(), params ) .thenCompose( unused -> getGeneratedValues( session ) ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java index 8f0a02b26..8e82a478f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveUniqueKeySelectingDelegate.java @@ -36,7 +36,6 @@ public class ReactiveUniqueKeySelectingDelegate extends UniqueKeySelectingDelega private final String[] uniqueKeyPropertyNames; private final Type[] uniqueKeyTypes; - public ReactiveUniqueKeySelectingDelegate( EntityPersister persister, String[] uniqueKeyPropertyNames, @@ -60,8 +59,8 @@ public CompletionStage reactivePerformMutation( PreparedStatementDetails details = new PrepareStatementDetailsAdaptor( statementDetails, statement, jdbcServices ); valueBindings.beforeStatement( details ); } ); - return ((ReactiveConnectionSupplier) session).getReactiveConnection() - .update( statementDetails.getSqlString(), params ) + return ( (ReactiveConnectionSupplier) session ).getReactiveConnection() + .update( statementDetails.getSqlString(), params ) .thenCompose( unused -> getGeneratedValues( entity, session ) ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java index 37722fcd1..d8a5e81fa 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveIdentityGenerator.java @@ -31,8 +31,8 @@ public class ReactiveIdentityGenerator extends IdentityGenerator { public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(EntityPersister persister) { final Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); /* - Hibernate ORM allows the selection of different strategies based on the property `hibernate.jdbc.use_get_generated_keys` - but Vertex driver does not support get generated keys. + Hibernate ORM allows the selection of different strategies based on the property `hibernate.jdbc.use_get_generated_keys`, + but the Vert.x driver does not support get generated keys. */ final boolean supportsInsertReturning = ReactiveGeneratedValuesHelper.supportsInsertReturning( dialect ); if ( supportsInsertReturning && noCustomSql( persister, INSERT ) ) { From 5eaac0cdbb7ca1c9af657a0b2acf130ba887194a Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Tue, 2 Sep 2025 19:37:15 +0200 Subject: [PATCH 6/7] [#1906] Clean up tests --- .../MutationDelegateIdentityTest.java | 160 ++++++++---------- ...MutationDelegateJoinedInheritanceTest.java | 56 ++---- .../reactive/MutationDelegateTest.java | 75 ++++---- 3 files changed, 116 insertions(+), 175 deletions(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java index 63bbe6fba..064ce2645 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java @@ -5,6 +5,10 @@ */ package org.hibernate.reactive; +import java.util.Collection; +import java.util.Date; +import java.util.List; + import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.Generated; import org.hibernate.annotations.NaturalId; @@ -37,9 +41,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import java.util.Collection; -import java.util.Date; -import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; @@ -67,11 +68,11 @@ protected Collection> annotatedEntities() { protected Configuration constructConfiguration() { Configuration configuration = super.constructConfiguration(); // Batch size is only enabled to make sure it's ignored when using mutation delegates - configuration.setProperty( AvailableSettings.STATEMENT_BATCH_SIZE, "5"); + configuration.setProperty( AvailableSettings.STATEMENT_BATCH_SIZE, "5" ); // Construct a tracker that collects query statements via the SqlStatementLogger framework. // Pass in configuration properties to hand off any actual logging properties - sqlTracker = new SqlStatementTracker( MutationDelegateIdentityTest::filter, configuration.getProperties() ); + sqlTracker = new SqlStatementTracker( s -> true, configuration.getProperties() ); return configuration; } @@ -85,20 +86,10 @@ protected void addServices(StandardServiceRegistryBuilder builder) { sqlTracker.registerService( builder ); } - private static boolean filter(String s) { - String[] accepted = { "insert ", "update ", "delete ", "select " }; - for ( String valid : accepted ) { - if ( s.toLowerCase().startsWith( valid ) ) { - return true; - } - } - return false; - } - @Test public void testInsertGeneratedIdentityOnly(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityOnly.class, MutationType.INSERT ); - + final int expectedQueriesSize = delegate instanceof AbstractReturningDelegate ? 1 : 2; final IdentityOnly entity = new IdentityOnly(); test( context, getMutinySessionFactory().withTransaction( s -> s @@ -107,16 +98,18 @@ public void testInsertGeneratedIdentityOnly(VertxTestContext context) { .invoke( () -> { assertThat( entity.getId() ).isNotNull(); assertThat( entity.getName() ).isNull(); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - assertExecutedQueriesCount( delegate instanceof AbstractReturningDelegate ? 1 : 2 ); } ) - )); + ) ); } @Test public void testInsertGeneratedValuesAndIdentity(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityAndValues.class, MutationType.INSERT ); - + final int expectedQueriesSize = delegate instanceof AbstractSelectingDelegate + ? 3 + : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2; final IdentityAndValues entity = new IdentityAndValues(); test( context, getMutinySessionFactory().withTransaction( s -> s @@ -125,38 +118,34 @@ public void testInsertGeneratedValuesAndIdentity(VertxTestContext context) { .invoke( () -> { assertThat( entity.getId() ).isNotNull(); assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - assertExecutedQueriesCount( - delegate instanceof AbstractSelectingDelegate - ? 3 - : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 - ); - }) - )); + } ) + ) ); } @Test public void testUpdateGeneratedValuesAndIdentity(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityAndValues.class, MutationType.UPDATE ); final IdentityAndValues entity = new IdentityAndValues(); + final int expectedQuerySize = delegate != null && delegate.supportsArbitraryValues() ? 3 : 4; - test( context, getMutinySessionFactory().withTransaction( s -> s - .persist( entity ) ) - .invoke( () -> sqlTracker.clear() ) + test( context, getMutinySessionFactory() + .withTransaction( s -> s.persist( entity ) ) + .invoke( sqlTracker::clear ) .chain( () -> getMutinySessionFactory().withTransaction( s -> s .find( IdentityAndValues.class, entity.getId() ) - .invoke( identityAndValues -> identityAndValues.setData( "changed" ) ) - ).chain( () -> getMutinySessionFactory().withTransaction( s -> s + .invoke( identityAndValues -> identityAndValues.setData( "changed" ) ) + ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s .find( IdentityAndValues.class, entity.getId() ) .invoke( identityAndValues -> { assertThat( entity.getUpdateDate() ).isNotNull(); - sqlTracker.getLoggedQueries().get( 0 ).startsWith( "select" ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); - assertExecutedQueriesCount( - delegate != null && delegate.supportsArbitraryValues() ? 3 : 4 - ); - }) - ) ) ) + assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expectedQuerySize ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .startsWith( "select" ).contains( "update" ); + } ) + ) ) ); } @@ -164,44 +153,42 @@ public void testUpdateGeneratedValuesAndIdentity(VertxTestContext context) { @DisabledFor(value = ORACLE, reason = "Vert.x driver doesn't support RowId type parameters") public void testInsertGeneratedValuesAndIdentityAndRowId(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityAndValuesAndRowId.class, MutationType.INSERT ); - - final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() - && getDialect().rowId( "" ) != null; + final int expectedQueriesSize = delegate instanceof AbstractSelectingDelegate + ? 3 + : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2; + final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() && getDialect().rowId( "" ) != null; final IdentityAndValuesAndRowId entity = new IdentityAndValuesAndRowId(); - test(context, getMutinySessionFactory().withTransaction( s -> s - .persist( entity ) - .call( s::flush ) - .invoke( () -> { - assertThat( entity.getId() ).isNotNull(); - assertThat( entity.getName() ).isEqualTo( "default_name" ); - - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - assertExecutedQueriesCount( - delegate instanceof AbstractSelectingDelegate - ? 3 - : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 - ); - - if ( shouldHaveRowId ) { - // assert row-id was populated in entity entry - final PersistenceContext pc = ( (MutinySessionImpl) s ).unwrap( ReactiveSession.class ) - .getPersistenceContext(); - final EntityEntry entry = pc.getEntry( entity ); - assertThat( entry.getRowId() ).isNotNull(); - } - sqlTracker.clear(); - entity.setData( "changed" ); - } ) - .call( s::flush ) - .invoke( () -> { - assertThat( entity.getUpdateDate() ).isNotNull(); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); - assertNumberOfOccurrenceInQueryNoSpace( 0, "id_column", shouldHaveRowId ? 0 : 1 ); - } ) - ).chain( () -> getMutinySessionFactory().withTransaction( s -> s - .find( IdentityAndValuesAndRowId.class, entity.getId() ) - .invoke( identityAndValuesAndRowId -> assertThat( identityAndValuesAndRowId.getUpdateDate() ).isNotNull() ) + test( context, getMutinySessionFactory() + .withTransaction( s -> s + .persist( entity ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getId() ).isNotNull(); + assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + if ( shouldHaveRowId ) { + // assert row-id was populated in entity entry + final PersistenceContext pc = ( (MutinySessionImpl) s ) + .unwrap( ReactiveSession.class ) + .getPersistenceContext(); + final EntityEntry entry = pc.getEntry( entity ); + assertThat( entry.getRowId() ).isNotNull(); + } + sqlTracker.clear(); + entity.setData( "changed" ); + } ) + .call( s::flush ) + .invoke( () -> { + assertThat( entity.getUpdateDate() ).isNotNull(); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertNumberOfOccurrenceInQueryNoSpace( 0, "id_column", shouldHaveRowId ? 0 : 1 ); + } ) + ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( IdentityAndValuesAndRowId.class, entity.getId() ) + .invoke( identityAndValuesAndRowId -> assertThat( identityAndValuesAndRowId.getUpdateDate() ).isNotNull() ) ) ) ); } @@ -209,34 +196,29 @@ public void testInsertGeneratedValuesAndIdentityAndRowId(VertxTestContext contex @Test @DisabledFor(value = ORACLE, reason = "Vert.x driver doesn't support RowId type parameters") public void testInsertGeneratedValuesAndIdentityAndRowIdAndNaturalId(VertxTestContext context) { - final GeneratedValuesMutationDelegate delegate = getDelegate( - IdentityAndValuesAndRowIdAndNaturalId.class, - MutationType.INSERT - ); + final GeneratedValuesMutationDelegate delegate = getDelegate( IdentityAndValuesAndRowIdAndNaturalId.class, MutationType.INSERT ); final IdentityAndValuesAndRowIdAndNaturalId entity = new IdentityAndValuesAndRowIdAndNaturalId( "naturalid_1" ); - + final boolean isUniqueKeyDelegate = delegate instanceof UniqueKeySelectingDelegate; + final int expectedQueriesSize = delegate == null || !delegate.supportsArbitraryValues() || isUniqueKeyDelegate ? 2 : 1; test(context, getMutinySessionFactory().withTransaction( s -> s .persist( entity ) .call( s::flush ) .invoke( () -> { assertThat( entity.getId() ).isNotNull(); assertThat( entity.getName() ).isEqualTo( "default_name" ); - + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - final boolean isUniqueKeyDelegate = delegate instanceof UniqueKeySelectingDelegate; - assertExecutedQueriesCount( - delegate == null || !delegate.supportsArbitraryValues() || isUniqueKeyDelegate ? 2 : 1 - ); + if ( isUniqueKeyDelegate ) { assertNumberOfOccurrenceInQueryNoSpace( 1, "data", 1 ); assertNumberOfOccurrenceInQueryNoSpace( 1, "id_column", 1 ); } - final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() - && getDialect().rowId( "" ) != null; + final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() && getDialect().rowId( "" ) != null; if ( shouldHaveRowId ) { // assert row-id was populated in entity entry - final PersistenceContext pc = ( (MutinySessionImpl) s ).unwrap( ReactiveSession.class ) + final PersistenceContext pc = ( (MutinySessionImpl) s ) + .unwrap( ReactiveSession.class ) .getPersistenceContext(); final EntityEntry entry = pc.getEntry( entity ); assertThat( entry.getRowId() ).isNotNull(); @@ -245,10 +227,6 @@ public void testInsertGeneratedValuesAndIdentityAndRowIdAndNaturalId(VertxTestCo ); } - private static void assertExecutedQueriesCount(int expected) { - assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expected ); - } - private static void assertNumberOfOccurrenceInQueryNoSpace(int queryNumber, String toCheck, int expectedNumberOfOccurrences) { String query = sqlTracker.getLoggedQueries().get( queryNumber ); int actual = query.split( toCheck, -1 ).length - 1; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java index f84be0b0a..59dec2e82 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java @@ -53,10 +53,7 @@ protected Configuration constructConfiguration() { // Construct a tracker that collects query statements via the SqlStatementLogger framework. // Pass in configuration properties to hand off any actual logging properties - sqlTracker = new SqlStatementTracker( - MutationDelegateJoinedInheritanceTest::filter, - configuration.getProperties() - ); + sqlTracker = new SqlStatementTracker( s -> true, configuration.getProperties() ); return configuration; } @@ -70,20 +67,12 @@ protected void addServices(StandardServiceRegistryBuilder builder) { sqlTracker.registerService( builder ); } - private static boolean filter(String s) { - String[] accepted = { "insert ", "update ", "delete ", "select " }; - for ( String valid : accepted ) { - if ( s.toLowerCase().startsWith( valid ) ) { - return true; - } - } - return false; - } - @Test public void testInsertBaseEntity(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( BaseEntity.class, MutationType.INSERT ); - + final int expectedQueriesSize = delegate instanceof AbstractSelectingDelegate + ? 3 + : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2; final BaseEntity entity = new BaseEntity(); test( context, getMutinySessionFactory().withTransaction( s -> s @@ -94,11 +83,7 @@ public void testInsertBaseEntity(VertxTestContext context) { assertThat( entity.getName() ).isEqualTo( "default_name" ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - assertExecutedQueriesCount( - delegate instanceof AbstractSelectingDelegate - ? 3 - : delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 - ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); } ) ) ); } @@ -116,20 +101,21 @@ public void testInsertChildEntity(VertxTestContext context) { assertThat( entity.getChildName() ).isEqualTo( "default_child_name" ); if ( delegate instanceof AbstractSelectingDelegate ) { + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 4 ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).contains( "insert" ); // Note: this is a current restriction, mutation delegates only retrieve generated values // on the "root" table, and we expect other values to be read through a subsequent select assertThat( sqlTracker.getLoggedQueries().get( 3 ) ).startsWith( "select" ); - assertExecutedQueriesCount( 4 ); + } else { + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 3 ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).contains( "insert" ); // Note: this is a current restriction, mutation delegates only retrieve generated values // on the "root" table, and we expect other values to be read through a subsequent select assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).startsWith( "select" ); - assertExecutedQueriesCount( 3 ); } } ) ) ); @@ -138,7 +124,7 @@ public void testInsertChildEntity(VertxTestContext context) { @Test public void testUpdateBaseEntity(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( BaseEntity.class, MutationType.UPDATE ); - + final int expectedQueriesSize = delegate != null && delegate.supportsArbitraryValues() ? 2 : 3; final BaseEntity entity = new BaseEntity(); test( context, getMutinySessionFactory().withTransaction( s -> s.persist( entity ) ) @@ -149,12 +135,9 @@ public void testUpdateBaseEntity(VertxTestContext context) { .call( s::flush ) .invoke( baseEntity -> { assertThat( entity.getUpdateDate() ).isNotNull(); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); - - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); - assertExecutedQueriesCount( - delegate != null && delegate.supportsArbitraryValues() ? 2 : 3 - ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .startsWith( "select" ).contains( "update" ); } ) ) ) @@ -174,13 +157,11 @@ public void testUpdateChildEntity(VertxTestContext context) { assertThat( entity.getUpdateDate() ).isNotNull(); assertThat( entity.getChildUpdateDate() ).isNotNull(); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 3 ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ).contains( "update" ); // Note: this is a current restriction, mutation delegates only retrieve generated values // on the "root" table, and we expect other values to be read through a subsequent select assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).startsWith( "select" ); - - assertExecutedQueriesCount( 3 ); } ) ) ) ); @@ -198,7 +179,7 @@ public void testGeneratedOnlyOnChild(VertxTestContext context) { .call( s::flush ) .invoke( () -> { assertThat( generatedChild.getName() ).isEqualTo( "child_name" ); - assertExecutedQueriesCount( 3 ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 3 ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "insert" ); assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).startsWith( "insert" ); assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).startsWith( "select" ); @@ -206,17 +187,12 @@ public void testGeneratedOnlyOnChild(VertxTestContext context) { )); } - private static void assertExecutedQueriesCount(int expected) { - assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expected ); - } - private static GeneratedValuesMutationDelegate getDelegate(Class entityClass, MutationType mutationType) { - GeneratedValuesMutationDelegate mutationDelegate = ( (SessionFactoryImplementor) factoryManager + return ( (SessionFactoryImplementor) factoryManager .getHibernateSessionFactory() ) .getMappingMetamodel() .findEntityDescriptor( entityClass ) .getMutationDelegate( mutationType ); - return mutationDelegate; } @Entity(name = "BaseEntity") diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java index 9a143214c..ccd903c17 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java @@ -51,7 +51,7 @@ public class MutationDelegateTest extends BaseReactiveTest { @Override protected Collection> annotatedEntities() { - return List.of( ValuesOnly.class,ValuesAndRowId.class, ValuesAndNaturalId.class ); + return List.of( ValuesOnly.class, ValuesAndRowId.class, ValuesAndNaturalId.class ); } @Override @@ -62,7 +62,7 @@ protected Configuration constructConfiguration() { // Construct a tracker that collects query statements via the SqlStatementLogger framework. // Pass in configuration properties to hand off any actual logging properties - sqlTracker = new SqlStatementTracker( MutationDelegateTest::filter, configuration.getProperties() ); + sqlTracker = new SqlStatementTracker( s -> true, configuration.getProperties() ); return configuration; } @@ -76,19 +76,10 @@ protected void addServices(StandardServiceRegistryBuilder builder) { sqlTracker.registerService( builder ); } - private static boolean filter(String s) { - String[] accepted = { "insert ", "update ", "delete ", "select " }; - for ( String valid : accepted ) { - if ( s.toLowerCase().startsWith( valid ) ) { - return true; - } - } - return false; - } - @Test public void testInsertGeneratedValues(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesOnly.class, MutationType.INSERT ); + final int expectedQueriesSize = delegate != null && delegate.supportsArbitraryValues() ? 1 : 2; ValuesOnly entity = new ValuesOnly( 1L ); test( context, getMutinySessionFactory().withTransaction( s -> s @@ -96,8 +87,8 @@ public void testInsertGeneratedValues(VertxTestContext context) { .call( s::flush ) .invoke( () -> { assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - assertExecutedQueriesCount( delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 ); } ) ) ); } @@ -105,6 +96,7 @@ public void testInsertGeneratedValues(VertxTestContext context) { @Test public void testUpdateGeneratedValues(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesOnly.class, MutationType.UPDATE ); + final int expectedQueriesSize = delegate != null && delegate.supportsArbitraryValues() ? 3 : 4; final ValuesOnly entity = new ValuesOnly( 2L ); test( context, getMutinySessionFactory() @@ -118,11 +110,11 @@ public void testUpdateGeneratedValues(VertxTestContext context) { .find( ValuesOnly.class, 2 ) ) .invoke( valuesOnly -> { assertThat( entity.getUpdateDate() ).isNotNull(); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); - assertExecutedQueriesCount(delegate != null && delegate.supportsArbitraryValues() ? 3 : 4 ); - } - ) + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .startsWith( "select" ) + .contains( "update" ); + } ) ) ); } @@ -131,6 +123,7 @@ public void testUpdateGeneratedValues(VertxTestContext context) { @DisabledFor(value = ORACLE, reason = "Vert.x driver doesn't support RowId type parameters") public void testGeneratedValuesAndRowId(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesAndRowId.class, MutationType.INSERT ); + final int expectedQueriesSize = delegate != null && delegate.supportsArbitraryValues() ? 1 : 2; final boolean shouldHaveRowId = delegate != null && delegate.supportsRowId() && getDialect().rowId( "" ) != null; final ValuesAndRowId entity = new ValuesAndRowId( 1L ); @@ -140,13 +133,12 @@ public void testGeneratedValuesAndRowId(VertxTestContext context) { .call( s::flush ) .invoke( () -> { assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - assertExecutedQueriesCount(delegate != null && delegate.supportsArbitraryValues() ? 1 : 2 ); - s.getFactory(); if ( shouldHaveRowId ) { // assert row-id was populated in entity entry - final PersistenceContext pc = ( (MutinySessionImpl) s ).unwrap( ReactiveSession.class ) - .getPersistenceContext(); + final PersistenceContext pc = ( (MutinySessionImpl) s ) + .unwrap( ReactiveSession.class ).getPersistenceContext(); final EntityEntry entry = pc.getEntry( entity ); assertThat( entry.getRowId() ).isNotNull(); } @@ -160,31 +152,31 @@ public void testGeneratedValuesAndRowId(VertxTestContext context) { } ) ) .chain( () -> getMutinySessionFactory().withTransaction( s -> s - .find( ValuesAndRowId.class, 1 ) - .invoke( valuesAndRowId -> assertThat( valuesAndRowId.getUpdateDate() ).isNotNull() ) - ) ) + .find( ValuesAndRowId.class, 1 ) + .invoke( valuesAndRowId -> assertThat( valuesAndRowId.getUpdateDate() ).isNotNull() ) + ) ) ); } @Test public void testInsertGeneratedValuesAndNaturalId(VertxTestContext context) { final GeneratedValuesMutationDelegate delegate = getDelegate( ValuesAndNaturalId.class, MutationType.INSERT ); + final boolean isUniqueKeyDelegate = delegate instanceof ReactiveUniqueKeySelectingDelegate; + int expectedQueriesSize = delegate == null || isUniqueKeyDelegate ? 2 : 1; final ValuesAndNaturalId entity = new ValuesAndNaturalId( 1L, "natural_1" ); test( context, getMutinySessionFactory().withTransaction( s -> s - .persist( entity ) - .chain( s::flush ) - .invoke( () -> { - assertThat( entity.getName() ).isEqualTo( "default_name" ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); - - final boolean isUniqueKeyDelegate = delegate instanceof ReactiveUniqueKeySelectingDelegate; - assertExecutedQueriesCount(delegate == null || isUniqueKeyDelegate ? 2 : 1); - if ( isUniqueKeyDelegate ) { - assertNumberOfOccurrenceInQueryNoSpace( 1, "data", 1 ); - assertNumberOfOccurrenceInQueryNoSpace( 1, "id_column", 0 ); - } - } ) + .persist( entity ) + .chain( s::flush ) + .invoke( () -> { + assertThat( entity.getName() ).isEqualTo( "default_name" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "insert" ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); + if ( isUniqueKeyDelegate ) { + assertNumberOfOccurrenceInQueryNoSpace( 1, "data", 1 ); + assertNumberOfOccurrenceInQueryNoSpace( 1, "id_column", 0 ); + } + } ) ) ); } @@ -195,17 +187,12 @@ private static void assertNumberOfOccurrenceInQueryNoSpace(int queryNumber, Stri assertThat( actual ).as( "number of " + toCheck ).isEqualTo( expectedNumberOfOccurrences ); } - private static void assertExecutedQueriesCount(int expected) { - assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expected ); - } - private static GeneratedValuesMutationDelegate getDelegate(Class entityClass, MutationType mutationType) { - GeneratedValuesMutationDelegate mutationDelegate = ( (SessionFactoryImplementor) factoryManager + return ( (SessionFactoryImplementor) factoryManager .getHibernateSessionFactory() ) .getMappingMetamodel() .findEntityDescriptor( entityClass ) .getMutationDelegate( mutationType ); - return mutationDelegate; } @Entity( name = "ValuesOnly" ) From b63b894a29538da6f9159de7f2f144b3f579dd14 Mon Sep 17 00:00:00 2001 From: Andrea Boriero Date: Wed, 3 Sep 2025 11:48:22 +0200 Subject: [PATCH 7/7] [#1906] Fix MutationDelegateTest, MutationDelegateJoinedInheritanceTest and MutationDelegateIdentityTest update query assertions --- .../reactive/MutationDelegateIdentityTest.java | 17 +++++++++++++---- .../MutationDelegateJoinedInheritanceTest.java | 7 ++++--- .../reactive/MutationDelegateTest.java | 18 +++++++++++++----- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java index 064ce2645..facd0f281 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateIdentityTest.java @@ -142,8 +142,8 @@ public void testUpdateGeneratedValuesAndIdentity(VertxTestContext context) { .invoke( identityAndValues -> { assertThat( entity.getUpdateDate() ).isNotNull(); assertThat( sqlTracker.getLoggedQueries().size() ).isEqualTo( expectedQuerySize ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) - .startsWith( "select" ).contains( "update" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); + assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).contains( "update " ); } ) ) ) ); @@ -182,7 +182,7 @@ public void testInsertGeneratedValuesAndIdentityAndRowId(VertxTestContext contex .call( s::flush ) .invoke( () -> { assertThat( entity.getUpdateDate() ).isNotNull(); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update " ); assertNumberOfOccurrenceInQueryNoSpace( 0, "id_column", shouldHaveRowId ? 0 : 1 ); } ) ) @@ -227,10 +227,19 @@ public void testInsertGeneratedValuesAndIdentityAndRowIdAndNaturalId(VertxTestCo ); } + /** + * The method is used to verify that the query logged at position `queryNumber` + * contains the `expectedNumberOfOccurrences` occurrences of the value `toCkeck`. + * e.g. `assertNumberOfOccurrenceInQueryNoSpace(1, "id", 3)` verifies the 1st executed query contains 3 occurrences of `id` + * + * @param queryNumber the position of the logged query + * @param toCheck the String we want to check the number of occurrences in the query + * @param expectedNumberOfOccurrences the number of occurrences of the `toCkeck` value we expect + */ private static void assertNumberOfOccurrenceInQueryNoSpace(int queryNumber, String toCheck, int expectedNumberOfOccurrences) { String query = sqlTracker.getLoggedQueries().get( queryNumber ); int actual = query.split( toCheck, -1 ).length - 1; - assertThat( actual ).as( "number of " + toCheck ).isEqualTo( expectedNumberOfOccurrences ); + assertThat( actual ).as( "Unexpected number of '" + toCheck + "' in the query " + query ).isEqualTo( expectedNumberOfOccurrences ); } private static GeneratedValuesMutationDelegate getDelegate(Class entityClass, MutationType mutationType) { diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java index 59dec2e82..4a33da868 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateJoinedInheritanceTest.java @@ -136,8 +136,8 @@ public void testUpdateBaseEntity(VertxTestContext context) { .invoke( baseEntity -> { assertThat( entity.getUpdateDate() ).isNotNull(); assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) - .startsWith( "select" ).contains( "update" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); + assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).contains( "update " ); } ) ) ) @@ -158,7 +158,8 @@ public void testUpdateChildEntity(VertxTestContext context) { assertThat( entity.getChildUpdateDate() ).isNotNull(); assertThat( sqlTracker.getLoggedQueries() ).hasSize( 3 ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ).contains( "update" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); + assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).contains( "update " ); // Note: this is a current restriction, mutation delegates only retrieve generated values // on the "root" table, and we expect other values to be read through a subsequent select assertThat( sqlTracker.getLoggedQueries().get( 2 ) ).startsWith( "select" ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java index ccd903c17..d4f8405ca 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutationDelegateTest.java @@ -111,9 +111,8 @@ public void testUpdateGeneratedValues(VertxTestContext context) { .invoke( valuesOnly -> { assertThat( entity.getUpdateDate() ).isNotNull(); assertThat( sqlTracker.getLoggedQueries() ).hasSize( expectedQueriesSize ); - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) - .startsWith( "select" ) - .contains( "update" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).startsWith( "select" ); + assertThat( sqlTracker.getLoggedQueries().get( 1 ) ).contains( "update " ); } ) ) ); @@ -147,7 +146,7 @@ public void testGeneratedValuesAndRowId(VertxTestContext context) { } ) .call( s::flush ) .invoke( () -> { - assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update" ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ).contains( "update " ); assertNumberOfOccurrenceInQueryNoSpace( 0, "id_column", shouldHaveRowId ? 0 : 1 ); } ) ) @@ -181,10 +180,19 @@ public void testInsertGeneratedValuesAndNaturalId(VertxTestContext context) { ); } + /** + * The method is used to verify that the query logged at position `queryNumber` + * contains the `expectedNumberOfOccurrences` occurrences of the value `toCkeck`. + * e.g. `assertNumberOfOccurrenceInQueryNoSpace(1, "id", 3)` verifies the 1st executed query contains 3 occurrences of `id` + * + * @param queryNumber the position of the logged query + * @param toCheck the String we want to check the number of occurrences in the query + * @param expectedNumberOfOccurrences the number of occurrences of the `toCkeck` value we expect + */ private static void assertNumberOfOccurrenceInQueryNoSpace(int queryNumber, String toCheck, int expectedNumberOfOccurrences) { String query = sqlTracker.getLoggedQueries().get( queryNumber ); int actual = query.split( toCheck, -1 ).length - 1; - assertThat( actual ).as( "number of " + toCheck ).isEqualTo( expectedNumberOfOccurrences ); + assertThat( actual ).as( "Unexpected number of '" + toCheck + "' in the query " + query ).isEqualTo( expectedNumberOfOccurrences ); } private static GeneratedValuesMutationDelegate getDelegate(Class entityClass, MutationType mutationType) {