From 7f87d9bf4c9976f89727e4208657cf3be11d2cc9 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 21 Apr 2025 20:47:13 +0200 Subject: [PATCH 1/2] [#2209] fix cast to ReactiveSession that broke StatelessSession --- .../ReactiveEntitySelectFetchInitializer.java | 4 +- .../reactive/MutinyStatelessSessionTest.java | 250 ++++++++++++++++++ 2 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyStatelessSessionTest.java diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java index 8f848d01b..6d5ea5418 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntitySelectFetchInitializer.java @@ -23,7 +23,7 @@ import org.hibernate.proxy.LazyInitializer; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; @@ -154,7 +154,7 @@ else if ( data.getInstance() == null ) { data.setState( State.INITIALIZED ); final String entityName = concreteDescriptor.getEntityName(); - return ( (ReactiveSession) session ).reactiveInternalLoad( + return ( (ReactiveQueryProducer) session ).reactiveInternalLoad( entityName, data.getEntityIdentifier(), true, diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyStatelessSessionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyStatelessSessionTest.java new file mode 100644 index 000000000..5d325b5f6 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyStatelessSessionTest.java @@ -0,0 +1,250 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.*; +import jakarta.persistence.criteria.*; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.junit.jupiter.api.Assertions.*; + +@Timeout(value = 10, timeUnit = MINUTES) + +public class MutinyStatelessSessionTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( GuineaPig.class ); + } + + @Test + public void testStatelessSession(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + test( context, getMutinySessionFactory().withStatelessSession( ss -> ss + .insert( pig ) + .chain( v -> ss.createSelectionQuery( "from GuineaPig where name=:n", GuineaPig.class ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.get( GuineaPig.class, pig.id ) ) + .chain( p -> { + assertThatPigsAreEqual( pig, p ); + p.name = "X"; + return ss.update( p ); + } ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "X" ) ) + .chain( v -> ss.createMutationQuery( "update GuineaPig set name='Y'" ).executeUpdate() ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "Y" ) ) + .chain( v -> ss.delete( pig ) ) + .chain( v -> ss.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList() ) + .invoke( list -> assertTrue( list.isEmpty() ) ) ) + ); + } + + @Test + public void testStatelessSessionWithNamed(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + test( context, getMutinySessionFactory().withStatelessSession( ss -> ss + .insert( pig ) + .chain( v -> ss.createNamedQuery( "findbyname", GuineaPig.class ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.get( GuineaPig.class, pig.id ) ) + .chain( p -> { + assertThatPigsAreEqual( pig, p ); + p.name = "X"; + return ss.update( p ); + } ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "X" ) ) + .chain( v -> ss.createNamedQuery( "updatebyname" ).executeUpdate() ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "Y" ) ) + .chain( v -> ss.delete( pig ) ) + .chain( v -> ss.createNamedQuery( "findall" ).getResultList() ) + .invoke( list -> assertTrue( list.isEmpty() ) ) ) + ); + } + + @Test + public void testStatelessSessionWithNative(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + test( context, getMutinySessionFactory().openStatelessSession() + .chain( ss -> ss.insert( pig ) + .chain( v -> ss + .createNativeQuery( "select * from Piggy where name=:n", GuineaPig.class ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.get( GuineaPig.class, pig.id ) ) + .chain( p -> { + assertThatPigsAreEqual( pig, p ); + p.name = "X"; + return ss.update( p ); + } ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "X" ) ) + .chain( v -> ss.createNativeQuery( "update Piggy set name='Y'" ) + .executeUpdate() ) + .invoke( rows -> assertEquals( 1, rows ) ) + .chain( v -> ss.refresh( pig ) ) + .invoke( v -> assertEquals( pig.name, "Y" ) ) + .chain( v -> ss.delete( pig ) ) + .chain( v -> ss.createNativeQuery( "select id from Piggy" ).getResultList() ) + .invoke( list -> assertTrue( list.isEmpty() ) ) + .chain( v -> ss.close() ) ) + ); + } + + @Test + public void testStatelessSessionCriteria(VertxTestContext context) { + GuineaPig pig = new GuineaPig( "Aloi" ); + GuineaPig mate = new GuineaPig("Aloina"); + pig.mate = mate; + + CriteriaBuilder cb = getSessionFactory().getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( GuineaPig.class ); + Root gp = query.from( GuineaPig.class ); + query.where( cb.equal( gp.get( "name" ), cb.parameter( String.class, "n" ) ) ); + query.orderBy( cb.asc( gp.get( "name" ) ) ); + + test( context, getMutinySessionFactory().openStatelessSession() + .chain( ss -> ss.insert(mate) + .chain( v -> ss.insert(pig) ) + .chain( v -> ss.createQuery( query ) + .setParameter( "n", pig.name ) + .getResultList() ) + .invoke( list -> { + assertFalse( list.isEmpty() ); + assertEquals( 1, list.size() ); + assertThatPigsAreEqual( pig, list.get( 0 ) ); + } ) + .chain( v -> ss.close() ) ) + ); + } + + @Test + public void testTransactionPropagation(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessSession( + session -> session.withTransaction( transaction -> session.createSelectionQuery( "from GuineaPig", GuineaPig.class ) + .getResultList() + .chain( list -> { + assertNotNull( session.currentTransaction() ); + assertFalse( session.currentTransaction().isMarkedForRollback() ); + session.currentTransaction().markForRollback(); + assertTrue( session.currentTransaction().isMarkedForRollback() ); + assertTrue( transaction.isMarkedForRollback() ); + return session.withTransaction( t -> { + assertTrue( t.isMarkedForRollback() ); + return session.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList(); + } ); + } ) ) + ) ); + } + + @Test + public void testSessionPropagation(VertxTestContext context) { + test( context, getMutinySessionFactory().withStatelessSession( + session -> session.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList() + .chain( list -> getMutinySessionFactory().withStatelessSession( s -> { + assertEquals( session, s ); + return s.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList(); + } ) ) + ) ); + } + + private void assertThatPigsAreEqual( GuineaPig expected, GuineaPig actual) { + assertNotNull( actual ); + assertEquals( expected.getId(), actual.getId() ); + assertEquals( expected.getName(), actual.getName() ); + } + + @NamedQuery(name = "findbyname", query = "from GuineaPig where name=:n") + @NamedQuery(name = "updatebyname", query = "update GuineaPig set name='Y'") + @NamedQuery(name = "findall", query = "from GuineaPig") + + @Entity(name = "GuineaPig") + @Table(name = "Piggy") + public static class GuineaPig { + @Id + @GeneratedValue + private Integer id; + private String name; + @Version + private int version; + + @ManyToOne + private GuineaPig mate; + + public GuineaPig() { + } + + public GuineaPig(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return id + ": " + name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + GuineaPig guineaPig = (GuineaPig) o; + return Objects.equals( name, guineaPig.name ); + } + + @Override + public int hashCode() { + return Objects.hash( name ); + } + } +} From 7e5194c44113c657a8a68653fcbd2eb6482b0e12 Mon Sep 17 00:00:00 2001 From: Gavin King Date: Mon, 21 Apr 2025 20:53:07 +0200 Subject: [PATCH 2/2] [#2209] fix more unnecessary casts to ReactiveSession at least some of these are not actually bugs, but some others look sus --- .../event/impl/DefaultReactiveDeleteEventListener.java | 3 ++- .../reactive/event/impl/DefaultReactiveLockEventListener.java | 3 ++- .../event/impl/DefaultReactiveRefreshEventListener.java | 4 ++-- .../internal/cte/ReactiveAbstractCteMutationHandler.java | 4 ++-- .../sqm/mutation/internal/cte/ReactiveCteInsertHandler.java | 4 ++-- .../graph/entity/internal/ReactiveEntityInitializerImpl.java | 4 ++-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java index 66151f03b..6b2392e1e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveDeleteEventListener.java @@ -48,6 +48,7 @@ import org.hibernate.reactive.event.ReactiveDeleteEventListener; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; @@ -199,7 +200,7 @@ private CompletionStage fetchAndDelete(DeleteEvent event, DeleteContext tr } //Object entity = persistenceContext.unproxyAndReassociate( event.getObject() ); - return ( (ReactiveSession) source ) + return ( (ReactiveQueryProducer) source ) .reactiveFetch( objectEvent, true ) .thenCompose( entity -> delete( event, transientEntities, entity ) ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java index 7832298e0..a75ac1194 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLockEventListener.java @@ -35,6 +35,7 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.session.ReactiveSession; import static org.hibernate.pretty.MessageHelper.infoString; @@ -76,7 +77,7 @@ public CompletionStage reactiveOnLock(LockEvent event) throws HibernateExc //TODO: if object was an uninitialized proxy, this is inefficient, // resulting in two SQL selects - return ( (ReactiveSession) source ).reactiveFetch( event.getObject(), true ) + return ( (ReactiveQueryProducer) source ).reactiveFetch( event.getObject(), true ) .thenCompose( entity -> reactiveOnLock( event, entity ) ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java index f5e5eb1b4..ba3edda4a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java @@ -37,7 +37,7 @@ import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.persister.entity.impl.ReactiveAbstractEntityPersister; -import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -84,7 +84,7 @@ public CompletionStage reactiveOnRefresh(RefreshEvent event, RefreshContex // Hibernate Reactive doesn't support detached instances in refresh() throw new IllegalArgumentException( "Unmanaged instance passed to refresh()" ); } - return ( (ReactiveSession) source ) + return ( (ReactiveQueryProducer) source ) .reactiveFetch( event.getObject(), true ) .thenCompose( entity -> reactiveOnRefresh( event, refreshedAlready, entity ) ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java index d1ea9026f..a75872f43 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java @@ -30,8 +30,8 @@ import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveAbstractMutationHandler; -import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.SqlAstTranslator; @@ -179,7 +179,7 @@ public MappingModelExpressible getResolvedMappingModelType(SqmParameter StandardReactiveSelectExecutor.INSTANCE.list( select, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java index aaf1be3e9..86f0a8bea 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java @@ -43,10 +43,10 @@ import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; import org.hibernate.query.sqm.tree.insert.SqmValues; +import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; -import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.spi.NavigablePath; @@ -575,7 +575,7 @@ public MappingModelExpressible getResolvedMappingModelType(SqmParameter StandardReactiveSelectExecutor.INSTANCE.list( select, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java index a00287eab..d19728fb1 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java @@ -25,7 +25,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; import org.hibernate.proxy.map.MapProxy; -import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; import org.hibernate.sql.results.graph.AssemblerCreationState; @@ -532,7 +532,7 @@ protected CompletionStage reactiveResolveEntityInstance(ReactiveEntityIn // If this initializer owns the entity, we have to remove the entity holder, // because the subsequent loading process will claim the entity session.getPersistenceContextInternal().removeEntityHolder( data.getEntityKey() ); - return ( (ReactiveSession) session ).reactiveInternalLoad( + return ( (ReactiveQueryProducer) session ).reactiveInternalLoad( data.getConcreteDescriptor().getEntityName(), data.getEntityKey().getIdentifier(), true,