From 1e2815fe92fe6abb801ef9cfa7bf9beee8e73651 Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Wed, 13 Sep 2023 20:22:15 +0200 Subject: [PATCH 1/2] [#1702] implement upsert() in StatelessSession --- .../org/hibernate/reactive/mutiny/Mutiny.java | 17 +++++ .../impl/MutinyStatelessSessionImpl.java | 10 +++ .../impl/ReactiveCoordinatorFactory.java | 16 +++++ .../entity/impl/ReactiveEntityPersister.java | 16 +++++ ...ReactiveJoinedSubclassEntityPersister.java | 14 ++++ ...eMergeCoordinatorStandardScopeFactory.java | 45 +++++++++++++ .../ReactiveSingleTableEntityPersister.java | 21 ++++++ .../ReactiveUnionSubclassEntityPersister.java | 16 +++++ .../mutation/ReactiveTableMergeBuilder.java | 65 +++++++++++++++++++ .../session/ReactiveStatelessSession.java | 2 + .../impl/ReactiveStatelessSessionImpl.java | 38 +++++++++++ .../org/hibernate/reactive/stage/Stage.java | 17 +++++ .../stage/impl/StageStatelessSessionImpl.java | 10 +++ 13 files changed, 287 insertions(+) create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java create mode 100644 hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveTableMergeBuilder.java 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 1e50a72b0..08f382c48 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 @@ -1689,6 +1689,23 @@ default Uni get(Class entityClass, Object id, LockModeType lockModeTyp */ Uni refresh(Object entity); + /** + * + * @param entity a detached entity instance + * + * @see org.hibernate.StatelessSession#upsert(String, Object) + */ + Uni 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) + */ + 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..878746b55 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 upsert( null, entity ); + } + + @Override + public Uni upsert(String entityName, Object entity) { + return uni( () -> delegate.reactiveUpsert( 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..6347f3fa5 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 @@ -223,6 +223,20 @@ public CompletionStage updateReactive( .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); } + @Override + public CompletionStage mergeReactive( + Object id, + Object[] fields, + int[] dirtyFields, + boolean hasDirtyCollection, + Object[] oldFields, + Object oldVersion, + Object object, + Object rowId, + SharedSessionContractImplementor session) { + return CompletionStages.nullFuture(); + } + @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/ReactiveMergeCoordinatorStandardScopeFactory.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java new file mode 100644 index 000000000..931160848 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveMergeCoordinatorStandardScopeFactory.java @@ -0,0 +1,45 @@ +/* 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.EntityTableMapping; +import org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard; +import org.hibernate.reactive.persister.entity.mutation.ReactiveScopedUpdateCoordinator; +import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinator; +import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinatorStandard; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.ast.builder.AbstractTableUpdateBuilder; +import org.hibernate.sql.model.ast.builder.TableMergeBuilder; + +public class ReactiveMergeCoordinatorStandardScopeFactory extends UpdateCoordinatorStandard + implements ReactiveUpdateCoordinator { + + public ReactiveMergeCoordinatorStandardScopeFactory( + AbstractEntityPersister entityPersister, + SessionFactoryImplementor factory) { + super( entityPersister, factory ); + } + + @Override + public ReactiveScopedUpdateCoordinator makeScopedCoordinator() { + return new ReactiveUpdateCoordinatorStandard( + entityPersister(), + factory(), + this.getStaticUpdateGroup(), + this.getBatchKey(), + this.getVersionUpdateGroup(), + this.getVersionUpdateBatchkey() + ); + } + + @Override + protected AbstractTableUpdateBuilder newTableUpdateBuilder(EntityTableMapping tableMapping) { + return new TableMergeBuilder<>( entityPersister(), tableMapping, factory() ); + } + +} 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..5bf8ff139 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() ); @@ -315,6 +320,22 @@ public CompletionStage updateReactive( .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); } + @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()) + .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..e64eae5f3 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 @@ -338,6 +338,22 @@ public CompletionStage updateReactive( .coordinateReactiveUpdate( object, id, rowId, values, oldVersion, oldValues, dirtyAttributeIndexes, hasDirtyCollection, session ); } + @Override + public CompletionStage mergeReactive( + Object id, + Object[] values, + int[] dirtyAttributeIndexes, + boolean hasDirtyCollection, + Object[] oldValues, + Object oldVersion, + Object object, + Object rowId, + SharedSessionContractImplementor session) { + 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/ReactiveTableMergeBuilder.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveTableMergeBuilder.java new file mode 100644 index 000000000..2509215a7 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveTableMergeBuilder.java @@ -0,0 +1,65 @@ +/* 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 java.util.List; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationTarget; +import org.hibernate.sql.model.TableMapping; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.model.ast.MutatingTableReference; +import org.hibernate.sql.model.ast.RestrictedTableMutation; +import org.hibernate.sql.model.ast.builder.AbstractTableUpdateBuilder; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.internal.TableUpdateNoSet; + +public class ReactiveTableMergeBuilder extends AbstractTableUpdateBuilder { + + public ReactiveTableMergeBuilder( + MutationTarget mutationTarget, + TableMapping tableMapping, + SessionFactoryImplementor sessionFactory) { + super( mutationTarget, tableMapping, sessionFactory ); + } + + public ReactiveTableMergeBuilder( + MutationTarget mutationTarget, + MutatingTableReference tableReference, + SessionFactoryImplementor sessionFactory) { + super( mutationTarget, tableReference, sessionFactory ); + } + + @SuppressWarnings("unchecked") + @Override + public RestrictedTableMutation buildMutation() { + final List valueBindings = combine( getValueBindings(), getKeyBindings(), getLobValueBindings() ); + if ( valueBindings.isEmpty() ) { + return (RestrictedTableMutation) new TableUpdateNoSet( getMutatingTable(), getMutationTarget() ); + } + + // TODO: add getMergeDetails() (from ORM) +// if ( getMutatingTable().getTableMapping().getUpdateDetails().getCustomSql() != null ) { +// return (RestrictedTableMutation) new TableUpdateCustomSql( +// getMutatingTable(), +// getMutationTarget(), +// getSqlComment(), +// valueBindings, +// getKeyRestrictionBindings(), +// getOptimisticLockBindings() +// ); +// } + + return (RestrictedTableMutation) new OptionalTableUpdate( + getMutatingTable(), + getMutationTarget(), + valueBindings, + getKeyRestrictionBindings(), + getOptimisticLockBindings() + ); + } +} 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..48c8b586d 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,8 @@ public interface ReactiveStatelessSession extends ReactiveQueryProducer, Reactiv CompletionStage reactiveUpdate(Object entity); + CompletionStage reactiveUpsert(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..ae759c0b0 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,43 @@ public CompletionStage reactiveRefresh(Object entity, LockMode lockMode) { .whenComplete( (v, e) -> getLoadQueryInfluencers().setInternalFetchProfile( previousFetchProfile ) ); } + //TODO: Add javadoc to reference ORM StatelessSessionImpl.upsert() + @Override + public CompletionStage reactiveUpsert(Object entity) { + final ReactiveEntityPersister persister = getEntityPersister( null, 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 5e1eb367a..ceca06a85 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 @@ -1762,6 +1762,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..13c5699d0 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 upsert( null, entity ); + } + + @Override + public CompletionStage upsert(String entityName, Object entity) { + return delegate.reactiveUpsert( entity ); + } + @Override public CompletionStage fetch(T association) { return delegate.reactiveFetch( association, false ); From e1a515205081df20271a93af7d2ca6c971d82247 Mon Sep 17 00:00:00 2001 From: Barry LaFond Date: Wed, 13 Sep 2023 20:22:38 +0200 Subject: [PATCH 2/2] [#1702] add test for upsert() --- .../org/hibernate/reactive/UpsertTest.java | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java 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..cea2d43eb --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java @@ -0,0 +1,163 @@ +/* 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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UpsertTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Record.class ); + } + + @AfterEach + public void cleanDb(VertxTestContext context) { + test( context, getSessionFactory() + .withTransaction( s -> s.createQuery( "delete from Record" ).executeUpdate() ) ); + } + + @Test + public void testUpsert( VertxTestContext context) { + Record r1 = new Record(123L,"hello earth"); + Record r2 = new Record(456L,"hello mars"); + Record r1changed = new Record(123L,"goodbye earth" ); + + test( context, getSessionFactory().withStatelessTransaction( ss -> ss + .upsert( r1 ) + .thenCompose( v -> ss.createQuery( "from Record where message=:n", Record.class ) + .setParameter( "n", r1.message ) + .getSingleResult() ) + .thenAccept( result -> { + assertNotNull( result ); + assertEquals( r1.message, result.message ); + } ) ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( s -> s + .upsert( r2 ) + .thenCompose( vv -> s.refresh( r2 ) ) + .thenAccept( vv -> assertEquals( "hello mars", r2.message ) ) + ) ) + // call upsert() with changed r1changed and refresh, then check for changed message + .thenCompose( vvv -> getSessionFactory().withStatelessSession( s -> s + .upsert(r1changed ) + .thenCompose( v -> { + s.refresh(); + return s.createQuery( "from Record where message=:n", Record.class ) + .setParameter( "n", r1changed.message ) + .getSingleResult(); + } ) + .thenAccept( result -> { + assertNotNull( result ); + assertEquals( r1changed.message, result.message ); + } ) ) + ) + ); + } + + @Test + public void testUpsertStage(VertxTestContext context) { + Record r1 = new Record( 123L, "hello earth" ); + Record r2 = new Record( 456L, "hello mars" ); + + test( context, getSessionFactory().withStatelessTransaction( ss -> ss + .upsert( r1 ) + .thenCompose( v -> ss.createQuery( "from Record where message=:n", Record.class ) + .setParameter( "n", r1.message ) + .getSingleResult() ) + .thenAccept( result -> { + assertNotNull( result ); + assertEquals( r1.message, result.message ); + } ) ) + .thenCompose( v -> getSessionFactory().withStatelessTransaction( s -> s + .upsert( r2 ) + .thenCompose( vv -> s.refresh( r2 ) ) + .thenAccept( vvv -> { + assertEquals( "hello mars", r2.message ); + // Change r1 message + r1.setMessage( "goodbye earth" ); + } ) + ) ) + // call upsert() and refresh, then check for changed message + .thenCompose( v -> getSessionFactory().withStatelessTransaction( s -> s + .upsert( r1 ) + .thenCompose( vv -> s.refresh( r1 ) ) + .thenAccept( vvv -> assertEquals( "goodbye earth", r1.message ) ) + ) ) + ); + } + + @Test + public void testUpsertMutiny( VertxTestContext context) { + Record r1 = new Record( 123L, "hello earth" ); + Record r2 = new Record( 456L, "hello mars" ); + + test( + context, + getMutinySessionFactory() + .withStatelessSession( ss -> ss.upsert( r1 ) + .chain( () -> ss.createQuery( "from Record where message=:n", Record.class ) + .setParameter( "n", r1.message ).getSingleResult() + .invoke( Assertions::assertNotNull ) + .invoke( result -> assertEquals( r1.message, result.message ) ) + ) ) + .call( v -> getMutinySessionFactory().withStatelessTransaction( s -> s + .upsert( r2 ) + .call( () -> s.refresh( r2 ) ) + .invoke( () -> { + assertEquals( "hello mars", r2.message ); + // Change r1 message + r1.setMessage( "goodbye earth" ); + } ) + ) ) + // call upsert() and refresh, then check for changed message + .call( v -> getMutinySessionFactory().withStatelessTransaction( s -> s + .upsert( r1 ) + .chain( () -> s.refresh( r1 ) ) + .invoke( () -> assertEquals( "goodbye earth", r1.message ) ) + ) ) + ); + } + + @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; + } + } +}