diff --git a/README.md b/README.md index 7e035bc5c..d6c1cf50b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Hibernate Reactive has been tested with: - CockroachDB 22.1 - MS SQL Server 2019 - Oracle 21.3 -- [Hibernate ORM][] 6.3.0.Final +- [Hibernate ORM][] 6.3.1.Final - [Vert.x Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) 4.4.5 - [Vert.x Reactive MySQL Client](https://vertx.io/docs/vertx-mysql-client/java/) 4.4.5 - [Vert.x Reactive Db2 Client](https://vertx.io/docs/vertx-db2-client/java/) 4.4.5 diff --git a/build.gradle b/build.gradle index bb05f89e6..3d1a41e8c 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,7 @@ version = projectVersion // ./gradlew clean build -PhibernateOrmVersion=5.6.15-SNAPSHOT ext { if ( !project.hasProperty('hibernateOrmVersion') ) { - hibernateOrmVersion = '6.3.0.Final' + hibernateOrmVersion = '6.3.1.Final' } if ( !project.hasProperty( 'hibernateOrmGradlePluginVersion' ) ) { // Same as ORM as default diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java index d821ed87f..a6b716a8c 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityDeleteAction.java @@ -64,12 +64,14 @@ public CompletionStage reactiveExecute() throws HibernateException { final boolean veto = isInstanceLoaded() && preDelete(); final Object ck = lockCacheItem(); - - final CompletionStage deleteStep = !isCascadeDeleteEnabled() && !veto - ? ( (ReactiveEntityPersister) persister ).deleteReactive( id, version, instance, session ) - : voidFuture(); - - return deleteStep.thenAccept( v -> { + return deleteStep( + veto, + (ReactiveEntityPersister) persister, + id, + version, + instance, + session + ).thenAccept( v -> { if ( isInstanceLoaded() ) { postDeleteLoaded( id, persister, session, instance, ck ); } @@ -84,4 +86,16 @@ public CompletionStage reactiveExecute() throws HibernateException { } } ); } + + private CompletionStage deleteStep( + boolean veto, + ReactiveEntityPersister persister, + Object id, + Object version, + Object instance, + SharedSessionContractImplementor session) { + return !isCascadeDeleteEnabled() && !veto + ? persister.deleteReactive( id, version, instance, session ) + : voidFuture(); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java index c8e904e88..49b69a0d3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java @@ -1736,6 +1736,28 @@ default Uni get(Class entityClass, Object id, LockModeType lockModeTyp */ Uni refresh(Object entity); + /** + * Use a SQL {@code merge into} statement to perform an upsert. + * + * @param entity a detached entity instance + * + * @see org.hibernate.StatelessSession#upsert(Object) + */ + @Incubating + Uni upsert(Object entity); + + /** + * Use a SQL {@code merge into} statement to perform an upsert. + * + * @param entityName The entityName for the entity to be merged + * @param entity a detached entity instance + * @throws org.hibernate.TransientObjectException is the entity is transient + * + * @see org.hibernate.StatelessSession#upsert(String, Object) + */ + @Incubating + Uni upsert(String entityName, Object entity); + /** * Refresh the entity instance state from the database. * diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java index ceaf5730d..b7dc5c26c 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinyStatelessSessionImpl.java @@ -163,6 +163,16 @@ public Uni refresh(Object entity) { return uni( () -> delegate.reactiveRefresh( entity ) ); } + @Override + public Uni upsert(Object entity) { + return uni( () -> delegate.reactiveUpsert( entity ) ); + } + + @Override + public Uni upsert(String entityName, Object entity) { + return uni( () -> delegate.reactiveUpsert( entityName, entity ) ); + } + @Override public Uni refreshAll(Object... entities) { return uni( () -> delegate.reactiveRefreshAll( entities ) ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java index e29273cad..aefc03ddc 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java @@ -44,4 +44,20 @@ public static ReactiveDeleteCoordinator buildDeleteCoordinator( SessionFactoryImplementor factory) { return new ReactiveDeleteCoordinator( entityPersister, factory ); } + + public static ReactiveUpdateCoordinator buildMergeCoordinator( + AbstractEntityPersister entityPersister, + SessionFactoryImplementor factory) { + // we only have updates to issue for entities with one or more singular attributes + final AttributeMappingsList attributeMappings = entityPersister.getAttributeMappings(); + for ( int i = 0; i < attributeMappings.size(); i++ ) { + AttributeMapping attributeMapping = attributeMappings.get( i ); + if ( attributeMapping instanceof SingularAttributeMapping ) { + return new ReactiveMergeCoordinatorStandardScopeFactory( entityPersister, factory ); + } + } + + // otherwise, nothing to update + return new ReactiveUpdateCoordinatorNoOp( entityPersister ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java index e96898781..ca42288fd 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java @@ -63,6 +63,22 @@ CompletionStage updateReactive( final Object rowId, final SharedSessionContractImplementor session); + /** + * Update the given instance state without blocking. + * + * @see EntityPersister#merge(Object, Object[], int[], boolean, Object[], Object, Object, Object, SharedSessionContractImplementor) + */ + CompletionStage mergeReactive( + final Object id, + final Object[] fields, + final int[] dirtyFields, + final boolean hasDirtyCollection, + final Object[] oldFields, + final Object oldVersion, + final Object object, + final Object rowId, + final SharedSessionContractImplementor session); + /** * Obtain a pessimistic lock without blocking */ 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 53150443d..e0213d25e 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 @@ -5,6 +5,10 @@ */ package org.hibernate.reactive.persister.entity.impl; +import java.sql.PreparedStatement; +import java.util.List; +import java.util.concurrent.CompletionStage; + import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; @@ -36,8 +40,8 @@ import org.hibernate.persister.entity.mutation.DeleteCoordinator; import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; -import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; import org.hibernate.reactive.persister.entity.mutation.ReactiveDeleteCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveInsertCoordinator; @@ -51,11 +55,6 @@ import org.hibernate.sql.results.graph.entity.internal.EntityResultJoinedSubclassImpl; import org.hibernate.type.EntityType; -import java.sql.PreparedStatement; -import java.util.List; -import java.util.concurrent.CompletionStage; - - /** * An {@link ReactiveEntityPersister} backed by {@link JoinedSubclassEntityPersister} * and {@link ReactiveAbstractEntityPersister}. @@ -223,6 +222,29 @@ public CompletionStage updateReactive( .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); } + /** + * Merge an Object + * + * @see #merge(Object, Object[], int[], boolean, Object[], Object, Object, Object, SharedSessionContractImplementor) + */ + @Override + public CompletionStage mergeReactive( + Object id, + Object[] values, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + Object[] oldValues, + Object oldVersion, + Object object, + Object rowId, + SharedSessionContractImplementor session) { + // This is different from Hibernate ORM because our reactive update coordinator cannot be share among + // multiple update operations + return ( (ReactiveUpdateCoordinator) getMergeCoordinator() ) + .makeScopedCoordinator() + .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); + } + @Override public CompletionStage> reactiveMultiLoad(K[] ids, EventSource session, MultiIdLoadOptions loadOptions) { return reactiveDelegate.multiLoad( ids, session, loadOptions ); @@ -248,6 +270,23 @@ public void update( throw LOG.nonReactiveMethodCall( "updateReactive" ); } + /** + * @see #mergeReactive(Object, Object[], int[], boolean, Object[], Object, Object, Object, SharedSessionContractImplementor) + */ + @Override + public void merge( + Object id, + Object[] values, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + Object[] oldValues, + Object oldVersion, + Object object, + Object rowId, + SharedSessionContractImplementor session) throws HibernateException { + throw LOG.nonReactiveMethodCall( "mergeReactive" ); + } + @Override public boolean check(int rows, Object id, int tableNumber, Expectation expectation, PreparedStatement statement, String sql) throws HibernateException { return super.check(rows, id, tableNumber, expectation, statement, sql); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java new file mode 100644 index 000000000..290e0efc8 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java @@ -0,0 +1,33 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.persister.entity.impl; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.mutation.MergeCoordinator; +import org.hibernate.reactive.persister.entity.mutation.ReactiveMergeCoordinator; +import org.hibernate.reactive.persister.entity.mutation.ReactiveScopedUpdateCoordinator; +import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinator; + +public class ReactiveMergeCoordinatorStandardScopeFactory extends MergeCoordinator + implements ReactiveUpdateCoordinator { + + public ReactiveMergeCoordinatorStandardScopeFactory(AbstractEntityPersister entityPersister, SessionFactoryImplementor factory) { + super( entityPersister, factory ); + } + + @Override + public ReactiveScopedUpdateCoordinator makeScopedCoordinator() { + return new ReactiveMergeCoordinator( + entityPersister(), + factory(), + this.getStaticUpdateGroup(), + this.getBatchKey(), + this.getVersionUpdateGroup(), + this.getVersionUpdateBatchkey() + ); + } +} 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 8d2291731..b144573ea 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 @@ -102,6 +102,11 @@ protected DeleteCoordinator buildDeleteCoordinator() { return ReactiveCoordinatorFactory.buildDeleteCoordinator( this, getFactory() ); } + @Override + protected UpdateCoordinator buildMergeCoordinator() { + return ReactiveCoordinatorFactory.buildMergeCoordinator( this, getFactory() ); + } + @Override public Generator getGenerator() throws HibernateException { return reactiveDelegate.reactive( super.getGenerator() ); @@ -218,6 +223,20 @@ public void update( throw LOG.nonReactiveMethodCall( "updateReactive" ); } + @Override + public void merge( + Object id, + Object[] values, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + Object[] oldValues, + Object oldVersion, + Object object, + Object rowId, + SharedSessionContractImplementor session) throws HibernateException { + throw LOG.nonReactiveMethodCall( "mergeReactive" ); + } + /** * Process properties generated with an insert * @@ -315,6 +334,29 @@ public CompletionStage updateReactive( .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); } + /** + * Merge an object + * + * @see SingleTableEntityPersister#merge(Object, Object[], int[], boolean, Object[], Object, Object, Object, SharedSessionContractImplementor) + */ + @Override + public CompletionStage mergeReactive( + final Object id, + final Object[] values, + int[] dirtyAttributeIndexes, + final boolean hasDirtyCollection, + final Object[] oldValues, + final Object oldVersion, + final Object object, + final Object rowId, + SharedSessionContractImplementor session) { + return ( (ReactiveUpdateCoordinator) getMergeCoordinator() ) + // This is different from Hibernate ORM because our reactive update coordinator cannot be share among + // multiple update operations + .makeScopedCoordinator() + .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); + } + @Override public CompletionStage> reactiveMultiLoad(K[] ids, EventSource session, MultiIdLoadOptions loadOptions) { return reactiveDelegate.multiLoad( ids, session, loadOptions ); 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 2269173da..87c82d6da 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 @@ -245,6 +245,23 @@ public void update( throw LOG.nonReactiveMethodCall( "updateReactive" ); } + /** + * @see #mergeReactive(Object, Object[], int[], boolean, Object[], Object, Object, Object, SharedSessionContractImplementor) + */ + @Override + public void merge( + Object id, + Object[] values, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + Object[] oldValues, + Object oldVersion, + Object object, + Object rowId, + SharedSessionContractImplementor session) throws HibernateException { + throw LOG.nonReactiveMethodCall( "mergeReactive" ); + } + /** * Process properties generated with an insert * @@ -338,6 +355,27 @@ public CompletionStage updateReactive( .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); } + /** + * @see #merge(Object, Object[], int[], boolean, Object[], Object, Object, Object, SharedSessionContractImplementor) + */ + @Override + public CompletionStage mergeReactive( + Object id, + Object[] values, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + Object[] oldValues, + Object oldVersion, + Object object, + Object rowId, + SharedSessionContractImplementor session) { + // This is different from Hibernate ORM because our reactive update coordinator cannot be share among + // multiple update operations + return ( (ReactiveUpdateCoordinator) getMergeCoordinator() ) + .makeScopedCoordinator() + .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); + } + @Override public CompletionStage> reactiveMultiLoad(K[] ids, EventSource session, MultiIdLoadOptions loadOptions) { return reactiveDelegate.multiLoad( ids, session, loadOptions ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java index 6f1d4f14a..3e6738d7b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java @@ -11,12 +11,10 @@ import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; import org.hibernate.engine.jdbc.mutation.MutationExecutor; -import org.hibernate.engine.jdbc.mutation.ParameterUsage; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.EntityRowIdMapping; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.mutation.DeleteCoordinator; import org.hibernate.persister.entity.mutation.EntityTableMapping; @@ -25,6 +23,7 @@ import org.hibernate.reactive.engine.jdbc.env.internal.ReactiveMutationExecutor; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.sql.model.MutationOperation; import org.hibernate.sql.model.MutationOperationGroup; import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; @@ -61,17 +60,18 @@ public CompletionStage coordinateReactiveDelete(Object entity, Object id, } @Override - protected void doDynamicDelete(Object entity, Object id, Object rowId, Object[] loadedState, SharedSessionContractImplementor session) { + protected void doDynamicDelete(Object entity, Object id, Object[] loadedState, SharedSessionContractImplementor session) { stage = new CompletableFuture<>(); - final MutationOperationGroup operationGroup = generateOperationGroup( loadedState, true, session ); + final MutationOperationGroup operationGroup = generateOperationGroup( null, loadedState, true, session ); final ReactiveMutationExecutor mutationExecutor = mutationExecutor( session, operationGroup ); - operationGroup.forEachOperation( (position, mutation) -> { + for ( int i = 0; i < operationGroup.getNumberOfOperations(); i++ ) { + final MutationOperation mutation = operationGroup.getOperation( i ); if ( mutation != null ) { final String tableName = mutation.getTableDetails().getTableName(); mutationExecutor.getPreparedStatementDetails( tableName ); } - } ); + } applyLocking( null, loadedState, mutationExecutor, session ); applyId( id, null, mutationExecutor, getStaticDeleteGroup(), session ); @@ -102,25 +102,29 @@ protected void applyId( MutationOperationGroup operationGroup, SharedSessionContractImplementor session) { final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); - final EntityRowIdMapping rowIdMapping = entityPersister().getRowIdMapping(); - operationGroup.forEachOperation( (position, jdbcMutation) -> { + for ( int position = 0; position < operationGroup.getNumberOfOperations(); position++ ) { + final MutationOperation jdbcMutation = operationGroup.getOperation( position ); final EntityTableMapping tableDetails = (EntityTableMapping) jdbcMutation.getTableDetails(); - breakDownIdJdbcValues( id, rowId, session, jdbcValueBindings, rowIdMapping, tableDetails ); + breakDownKeyJdbcValues( id, rowId, session, jdbcValueBindings, tableDetails ); final PreparedStatementDetails statementDetails = mutationExecutor.getPreparedStatementDetails( tableDetails.getTableName() ); if ( statementDetails != null ) { PreparedStatementAdaptor.bind( statement -> { - PrepareStatementDetailsAdaptor detailsAdaptor = new PrepareStatementDetailsAdaptor( statementDetails, statement, session.getJdbcServices() ); + PrepareStatementDetailsAdaptor detailsAdaptor = new PrepareStatementDetailsAdaptor( + statementDetails, + statement, + session.getJdbcServices() + ); // force creation of the PreparedStatement //noinspection resource detailsAdaptor.resolveStatement(); } ); } - } ); + } } @Override - protected void doStaticDelete(Object entity, Object id, Object[] loadedState, Object version, SharedSessionContractImplementor session) { + protected void doStaticDelete(Object entity, Object id, Object rowId, Object[] loadedState, Object version, SharedSessionContractImplementor session) { stage = new CompletableFuture<>(); final boolean applyVersion = entity != null; final MutationOperationGroup operationGroupToUse = entity == null @@ -128,18 +132,19 @@ protected void doStaticDelete(Object entity, Object id, Object[] loadedState, Ob : getStaticDeleteGroup(); final ReactiveMutationExecutor mutationExecutor = mutationExecutor( session, operationGroupToUse ); - getStaticDeleteGroup().forEachOperation( (position, mutation) -> { + for ( int position = 0; position < getStaticDeleteGroup().getNumberOfOperations(); position++ ) { + final MutationOperation mutation = getStaticDeleteGroup().getOperation( position ); if ( mutation != null ) { mutationExecutor.getPreparedStatementDetails( mutation.getTableDetails().getTableName() ); } - } ); + } if ( applyVersion ) { applyLocking( version, null, mutationExecutor, session ); } final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); bindPartitionColumnValueBindings( loadedState, session, jdbcValueBindings ); - applyId( id, null, mutationExecutor, getStaticDeleteGroup(), session ); + applyId( id, rowId, mutationExecutor, getStaticDeleteGroup(), session ); mutationExecutor.executeReactive( entity, null, @@ -158,38 +163,6 @@ protected void doStaticDelete(Object entity, Object id, Object[] loadedState, Ob .whenComplete( this::complete ); } - /** - * Copy and paste of the on in ORM - */ - private static void breakDownIdJdbcValues( - Object id, - Object rowId, - SharedSessionContractImplementor session, - JdbcValueBindings jdbcValueBindings, - EntityRowIdMapping rowIdMapping, - EntityTableMapping tableDetails) { - if ( rowId != null && rowIdMapping != null && tableDetails.isIdentifierTable() ) { - jdbcValueBindings.bindValue( - rowId, - tableDetails.getTableName(), - rowIdMapping.getRowIdName(), - ParameterUsage.RESTRICT - ); - } - else { - tableDetails.getKeyMapping().breakDownKeyJdbcValues( - id, - (jdbcValue, columnMapping) -> jdbcValueBindings.bindValue( - jdbcValue, - tableDetails.getTableName(), - columnMapping.getColumnName(), - ParameterUsage.RESTRICT - ), - session - ); - } - } - private void complete(Object o, Throwable throwable) { if ( throwable != null ) { stage.toCompletableFuture().completeExceptionally( throwable ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveMergeCoordinator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveMergeCoordinator.java new file mode 100644 index 000000000..815842ea8 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveMergeCoordinator.java @@ -0,0 +1,36 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.persister.entity.mutation; + +import org.hibernate.engine.jdbc.batch.spi.BatchKey; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationOperationGroup; +import org.hibernate.sql.model.ast.builder.AbstractTableUpdateBuilder; +import org.hibernate.sql.model.ast.builder.TableMergeBuilder; + +/** + * @see org.hibernate.persister.entity.mutation.MergeCoordinator + * @see org.hibernate.reactive.persister.entity.impl.ReactiveMergeCoordinatorStandardScopeFactory + */ +public class ReactiveMergeCoordinator extends ReactiveUpdateCoordinatorStandard { + public ReactiveMergeCoordinator( + AbstractEntityPersister entityPersister, + SessionFactoryImplementor factory, + MutationOperationGroup staticUpdateGroup, + BatchKey batchKey, + MutationOperationGroup versionUpdateGroup, + BatchKey versionUpdateBatchkey) { + super( entityPersister, factory, staticUpdateGroup, batchKey, versionUpdateGroup, versionUpdateBatchkey ); + } + + @Override + protected AbstractTableUpdateBuilder newTableUpdateBuilder(EntityTableMapping tableMapping) { + return new TableMergeBuilder<>( entityPersister(), tableMapping, factory() ); + } +} 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 adb684137..390d81b83 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 @@ -184,7 +184,7 @@ public CompletionStage updateBatch(String sql, List parametersBatc int i = 0; RowSet resultNext = result; - if ( parametersBatch.size() > 0 ) { + if ( !parametersBatch.isEmpty() ) { final RowIterator iterator = resultNext.iterator(); if ( iterator.hasNext() ) { while ( iterator.hasNext() ) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveStatelessSession.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveStatelessSession.java index 61bea2a6c..7b3278992 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveStatelessSession.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveStatelessSession.java @@ -40,6 +40,10 @@ public interface ReactiveStatelessSession extends ReactiveQueryProducer, Reactiv CompletionStage reactiveUpdate(Object entity); + CompletionStage reactiveUpsert(Object entity); + + CompletionStage reactiveUpsert(String entityName, Object entity); + CompletionStage reactiveRefresh(Object entity); CompletionStage reactiveRefresh(Object entity, LockMode lockMode); 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 d7fa353b5..05532b4e1 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 @@ -12,6 +12,7 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.TransientObjectException; import org.hibernate.UnknownEntityTypeException; import org.hibernate.UnknownProfileException; import org.hibernate.UnresolvableObjectException; @@ -367,6 +368,57 @@ public CompletionStage reactiveRefresh(Object entity, LockMode lockMode) { .whenComplete( (v, e) -> getLoadQueryInfluencers().setInternalFetchProfile( previousFetchProfile ) ); } + + /** + * @see StatelessSessionImpl#upsert(Object) + */ + @Override + public CompletionStage reactiveUpsert(Object entity) { + checkOpen(); + return reactiveUpsert( null, entity ); + } + + /** + * @see StatelessSessionImpl#upsert(String, Object) + */ + @Override + public CompletionStage reactiveUpsert(String entityName, Object entity) { + checkOpen(); + final ReactiveEntityPersister persister = getEntityPersister( entityName, entity ); + Object id = persister.getIdentifier( entity, this ); + Boolean knownTransient = persister.isTransient( entity, this ); + if ( knownTransient != null && knownTransient ) { + throw new TransientObjectException( + "Object passed to upsert() has a null identifier: " + + persister.getEntityName() ); +// final Generator generator = persister.getGenerator(); +// if ( !generator.generatedOnExecution() ) { +// id = ( (BeforeExecutionGenerator) generator).generate( this, entity, null, INSERT ); +// } + } + final Object[] state = persister.getValues( entity ); + final Object oldVersion; + if ( persister.isVersioned() ) { + oldVersion = persister.getVersion( entity ); + if ( oldVersion == null ) { + if ( seedVersion( entity, state, persister, this ) ) { + persister.setValues( entity, state ); + } + } + else { + final Object newVersion = incrementVersion( entity, oldVersion, persister, this ); + setVersion( state, newVersion, persister ); + persister.setValues( entity, state ); + } + } + else { + oldVersion = null; + } + + return persister + .mergeReactive( id, state, null, false, null, oldVersion, entity, null, this ); + } + @Override public CompletionStage reactiveInsertAll(Object... entities) { return loop( entities, batchingHelperSession::reactiveInsert ) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java index 044a9d52b..319c1c8f5 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java @@ -1814,6 +1814,23 @@ default CompletionStage refresh(Object entity, LockModeType lockModeType) return refresh( entity, convertToLockMode(lockModeType) ); } + /** + * + * @param entity a detached entity instance + * + * @see org.hibernate.StatelessSession#upsert(Object) + */ + CompletionStage upsert(Object entity); + + /** + * + * @param entityName The entityName for the entity to be merged + * @param entity a detached entity instance + * + * @see org.hibernate.StatelessSession#upsert(String, Object) + */ + CompletionStage upsert(String entityName, Object entity); + /** * Asynchronously fetch an association that's configured for lazy loading. * diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java index 51ccae294..1bacfa354 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageStatelessSessionImpl.java @@ -117,6 +117,16 @@ public CompletionStage refresh(Object entity, LockMode lockMode) { return delegate.reactiveRefresh( entity, lockMode ); } + @Override + public CompletionStage upsert(Object entity) { + return delegate.reactiveUpsert( entity ); + } + + @Override + public CompletionStage upsert(String entityName, Object entity) { + return delegate.reactiveUpsert( entityName, entity ); + } + @Override public CompletionStage fetch(T association) { return delegate.reactiveFetch( association, false ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/RowIdUpdateTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/RowIdUpdateAndDeleteTest.java similarity index 69% rename from hibernate-reactive-core/src/test/java/org/hibernate/reactive/RowIdUpdateTest.java rename to hibernate-reactive-core/src/test/java/org/hibernate/reactive/RowIdUpdateAndDeleteTest.java index 5abe53f35..af84be1e0 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/RowIdUpdateTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/RowIdUpdateAndDeleteTest.java @@ -33,9 +33,9 @@ import static org.hibernate.reactive.testing.DBSelectionExtension.skipTestsFor; /** - * Adapted from the test with the same name in Hibernate ORM: {@literal org.hibernate.orm.test.rowid.RowIdUpdateTest} + * Adapted from the test with the same name in Hibernate ORM: {@literal org.hibernate.orm.test.rowid.RowIdUpdateAndDeleteTest} */ -public class RowIdUpdateTest extends BaseReactiveTest { +public class RowIdUpdateAndDeleteTest extends BaseReactiveTest { // Db2: Exception: IllegalStateException: Needed to have 6 in buffer but only had 0 // Oracle: Vert.x driver doesn't support RowId type parameters @@ -52,7 +52,7 @@ protected Collection> annotatedEntities() { @Override protected Configuration constructConfiguration() { Configuration configuration = super.constructConfiguration(); - sqlTracker = new SqlStatementTracker( RowIdUpdateTest::isUsingRowId, configuration.getProperties() ); + sqlTracker = new SqlStatementTracker( RowIdUpdateAndDeleteTest::isRowIdQuery, configuration.getProperties() ); return configuration; } @@ -61,15 +61,17 @@ protected void addServices(StandardServiceRegistryBuilder builder) { sqlTracker.registerService( builder ); } - private static boolean isUsingRowId(String s) { - return s.toLowerCase().startsWith( "update" ); + private static boolean isRowIdQuery(String s) { + return s.toLowerCase().startsWith( "update" ) || s.toLowerCase().startsWith( "delete" ); } @BeforeEach public void prepareDb(VertxTestContext context) { test( context, getMutinySessionFactory().withTransaction( session -> session.persistAll( new SimpleEntity( 1L, "initial_status" ), - new ParentEntity( 2L, new SimpleEntity( 2L, "initial_status" ) ) + new ParentEntity( 2L, new SimpleEntity( 2L, "initial_status" ) ), + new SimpleEntity( 11L, "to_delete" ), + new ParentEntity( 12L, new SimpleEntity( 12L, "to_delete" ) ) ) ) ); } @@ -86,11 +88,30 @@ public void testSimpleUpdateSameTransaction(VertxTestContext context) { .chain( () -> getMutinySessionFactory() .withSession( session -> session.find( SimpleEntity.class, 3L ) ) ) // the update should have used the primary key, as the row-id value is not available - .invoke( RowIdUpdateTest::shouldUsePrimaryKey ) + .invoke( RowIdUpdateAndDeleteTest::shouldUsePrimaryKeyForUpdate ) .invoke( entity -> assertThat( entity ).hasFieldOrPropertyWithValue( "status", "new_status" ) ) ); } + @Test + public void testSimpleDeleteSameTransaction(VertxTestContext context) { + sqlTracker.clear(); + test( context, getMutinySessionFactory() + .withTransaction( session -> { + final SimpleEntity simpleEntity = new SimpleEntity( 13L, "to_delete" ); + return session.persist( simpleEntity ) + .call( session::flush ) + .call( () -> session.remove( simpleEntity ) ) + .invoke( () -> simpleEntity.setStatus( "new_status" ) ); + } ) + .chain( () -> getMutinySessionFactory() + .withSession( session -> session.find( SimpleEntity.class, 13L ) ) ) + // the update should have used the primary key, as the row-id value is not available + .invoke( RowIdUpdateAndDeleteTest::shouldUsePrimaryKeyForDelete ) + .invoke( entity -> assertThat( entity ).isNull() ) + ); + } + @Test public void testRelatedUpdateSameTransaction(VertxTestContext context) { sqlTracker.clear(); @@ -105,11 +126,26 @@ public void testRelatedUpdateSameTransaction(VertxTestContext context) { .chain( () -> getMutinySessionFactory() .withSession( session -> session.find( SimpleEntity.class, 4L ) ) ) // the update should have used the primary key, as the row-id value is not available - .invoke( RowIdUpdateTest::shouldUsePrimaryKey ) + .invoke( RowIdUpdateAndDeleteTest::shouldUsePrimaryKeyForUpdate ) .invoke( entity -> assertThat( entity ).hasFieldOrPropertyWithValue( "status", "new_status" ) ) ); } + @Test + public void testSimpleDeleteDifferentTransaction(VertxTestContext context) { + sqlTracker.clear(); + test( context, getMutinySessionFactory() + .withTransaction( session -> session + .find( SimpleEntity.class, 11L ) + .call( session::remove ) + ) + .chain( () -> getMutinySessionFactory() + .withSession( session -> session.find( SimpleEntity.class, 11L ) ) ) + .invoke( RowIdUpdateAndDeleteTest::shouldUseRowIdForDelete ) + .invoke( entity -> assertThat( entity ).isNull() ) + ); + } + @Test public void testSimpleUpdateDifferentTransaction(VertxTestContext context) { sqlTracker.clear(); @@ -120,7 +156,7 @@ public void testSimpleUpdateDifferentTransaction(VertxTestContext context) { ) .chain( () -> getMutinySessionFactory() .withSession( session -> session.find( SimpleEntity.class, 1L ) ) ) - .invoke( RowIdUpdateTest::shouldUseRowId ) + .invoke( RowIdUpdateAndDeleteTest::shouldUseRowIdForUpdate ) .invoke( entity -> assertThat( entity ).hasFieldOrPropertyWithValue( "status", "new_status" ) ) ); } @@ -133,7 +169,7 @@ public void testRelatedUpdateRelatedDifferentTransaction(VertxTestContext contex .find( ParentEntity.class, 2L ) .invoke( entity -> entity.getChild().setStatus( "new_status" ) ) ) - .invoke( RowIdUpdateTest::shouldUseRowId ) + .invoke( RowIdUpdateAndDeleteTest::shouldUseRowIdForUpdate ) .chain( () -> getMutinySessionFactory() .withSession( session -> session.find( SimpleEntity.class, 2L ) ) ) .invoke( entity -> assertThat( entity ) @@ -142,13 +178,19 @@ public void testRelatedUpdateRelatedDifferentTransaction(VertxTestContext contex ); } - private static void shouldUsePrimaryKey() { + private static void shouldUsePrimaryKeyForUpdate() { assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) .matches( "update SimpleEntity set status=.+ where primary_key=.+" ); } - private static void shouldUseRowId() { + private static void shouldUsePrimaryKeyForDelete() { + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "delete from SimpleEntity where primary_key=.+" ); + } + + private static void shouldUseRowIdForUpdate() { // Not all databases have a rowId column String rowId = getDialect().rowId( "" ); String column = rowId == null ? "primary_key" : rowId; @@ -157,6 +199,15 @@ private static void shouldUseRowId() { .matches( "update SimpleEntity set status=.+ where " + column + "=.+" ); } + private static void shouldUseRowIdForDelete() { + // Not all databases have a rowId column + String rowId = getDialect().rowId( "" ); + String column = rowId == null ? "primary_key" : rowId; + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + assertThat( sqlTracker.getLoggedQueries().get( 0 ) ) + .matches( "delete from SimpleEntity where " + column + "=.+" ); + } + @Entity(name = "SimpleEntity") @Table(name = "SimpleEntity") @RowId diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java new file mode 100644 index 000000000..94c94f55c --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java @@ -0,0 +1,203 @@ +/* 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.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.hibernate.reactive.testing.DBSelectionExtension; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.COCKROACHDB; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MYSQL; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; +import static org.hibernate.reactive.testing.DBSelectionExtension.skipTestsFor; + +/** + * Same as Hibernate ORM org.hibernate.orm.test.stateless.UpsertTest + *

+ * These tests are in a separate class because we need to skip the execution on some databases, + * but once this has been resolved, they could be in {@link ReactiveStatelessSessionTest}. + *

+ */ +@Timeout(value = 10, timeUnit = MINUTES) +public class UpsertTest extends BaseReactiveTest { + + /** + * Something is missing in HR to make it work for these databases. + */ + @RegisterExtension + public DBSelectionExtension dbSelection = skipTestsFor( COCKROACHDB, DB2, MARIA, MYSQL, ORACLE ); + + @Override + protected Collection> annotatedEntities() { + return List.of( Record.class ); + } + + @Test + public void testMutinyUpsert(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( ss -> ss + .upsert( new Record( 123L, "hello earth" ) ) + .call( () -> ss.upsert( new Record( 456L, "hello mars" ) ) ) + ) + .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .invoke( results -> assertThat( results ).containsExactly( + new Record( 123L, "hello earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + .call( () -> getMutinySessionFactory().withStatelessTransaction( ss -> ss + .upsert( new Record( 123L, "goodbye earth" ) ) + ) ) + .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .invoke( results -> assertThat( results ).containsExactly( + new Record( 123L, "goodbye earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + ); + } + + @Test + public void testMutinyUpsertWithEntityName(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessTransaction( ss -> ss + .upsert( Record.class.getName(), new Record( 123L, "hello earth" ) ) + .call( () -> ss.upsert( Record.class.getName(), new Record( 456L, "hello mars" ) ) ) + ) + .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .invoke( results -> assertThat( results ).containsExactly( + new Record( 123L, "hello earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + .call( () -> getMutinySessionFactory().withStatelessTransaction( ss -> ss + .upsert( Record.class.getName(), new Record( 123L, "goodbye earth" ) ) + ) ) + .call( v -> getMutinySessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .invoke( results -> assertThat( results ).containsExactly( + new Record( 123L, "goodbye earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + ); + } + + @Test + public void testStageUpsert(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( ss -> ss + .upsert( new Record( 123L, "hello earth" ) ) + .thenCompose( v -> ss.upsert( new Record( 456L, "hello mars" ) ) ) + ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .thenAccept( results -> assertThat( results ).containsExactly( + new Record( 123L, "hello earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss + .upsert( new Record( 123L, "goodbye earth" ) ) + ) ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .thenAccept( results -> assertThat( results ).containsExactly( + new Record( 123L, "goodbye earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + ); + } + + @Test + public void testStageUpsertWithEntityName(VertxTestContext context) { + test( context, getSessionFactory().withStatelessTransaction( ss -> ss + .upsert( Record.class.getName(), new Record( 123L, "hello earth" ) ) + .thenCompose( v -> ss.upsert( Record.class.getName(), new Record( 456L, "hello mars" ) ) ) + ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .thenAccept( results -> assertThat( results ).containsExactly( + new Record( 123L, "hello earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss + .upsert( Record.class.getName(), new Record( 123L, "goodbye earth" ) ) + ) ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( ss -> ss + .createQuery( "from Record order by id", Record.class ).getResultList() ) + .thenAccept( results -> assertThat( results ).containsExactly( + new Record( 123L, "goodbye earth" ), + new Record( 456L, "hello mars" ) + ) ) + ) + ); + } + + @Entity(name = "Record") + @Table(name = "Record") + public static class Record { + @Id + public Long id; + public String message; + + Record(Long id, String message) { + this.id = id; + this.message = message; + } + + Record() { + } + + public Long getId() { + return id; + } + + public String getMessage() { + return message; + } + + public void setMessage(String msg) { + message = msg; + } + + // Equals and HashCode for simplifying the test assertions, + // not to be taken as an example or for production. + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Record record = (Record) o; + return Objects.equals( id, record.id ) && Objects.equals( message, record.message ); + } + + @Override + public int hashCode() { + return Objects.hash( id, message ); + } + } +}