From f3db8e0e93b5d71c8069c66b2943952beed6a898 Mon Sep 17 00:00:00 2001 From: Luca Molteni Date: Thu, 16 Oct 2025 10:40:22 +0200 Subject: [PATCH 1/4] Add hibernate-reactive-transactions extension for @Transactional support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/quarkusio/quarkus/issues/47698 Introduces a new extension enabling @Transactional annotation support for Hibernate Reactive. This allows developers to use familiar transaction semantics with reactive database operations. Key implementation details: * New hibernate-reactive-transactions extension with dedicated CDI interceptors * TransactionalContextPool wraps the SQL client pool to lazily open transactions based on Vert.x context flags set by the @Transactional interceptor * TransactionalInterceptorBase manages the complete transaction lifecycle (begin, commit, rollback) within the reactive Uni pipeline * TransactionalContextConnection wrapper prevents premature connection closure before transaction commit/rollback * OpenedSessionState refactored from Panache to centrally manage session lifecycle and caching (possibly to be reused in Panache as well) * Validation prevents mixing @Transactional with @WithTransaction and @WithSessionOnDemand annotations to avoid conflicting transaction semantics * Disable JTA interceptor execution for methods returning Uni to prevent mixups Limitations: * Currently supports only Transactional.TxType.REQUIRED, other TxType values throw UnsupportedOperationException Dependencies updated: * hibernate-orm.version → 7.2.0.CR1 * hibernate-reactive.version → 3.2.0-SNAPSHOT --- bom/application/pom.xml | 10 ++ .../StatelessSessionLazyDelegator.java | 12 ++ .../runtime/boot/FastBootMetadataBuilder.java | 9 +- .../customized/QuarkusProxyFactory.java | 4 +- .../service/FlatClassLoaderService.java | 6 +- .../QuarkusRuntimeInitDialectFactory.java | 4 +- .../session/TransactionScopedSession.java | 8 + .../TransactionScopedStatelessSession.java | 16 ++ .../deployment/pom.xml | 93 +++++++++++ ...ibernateReactiveTransactionsProcessor.java | 63 +++++++ .../transactions/test/ConcurrencyTest.java | 92 +++++++++++ .../test/DisableJTATransactionTest.java | 47 ++++++ .../reactive/transactions/test/Hero.java | 34 ++++ .../HibernateReactiveTransactionsTest.java | 119 ++++++++++++++ ...upportOnlyRequiredTransactionTypeTest.java | 91 +++++++++++ .../test/mixing/MixReactiveBlockingTest.java | 97 +++++++++++ .../MixStatelessStatefulSessionTest.java | 63 +++++++ ...nDemandAndTransactionalSameMethodTest.java | 38 +++++ .../mixing/MixWithSessionOnDemandTest.java | 65 ++++++++ .../test/mixing/MixWithTransactionTest.java | 65 ++++++++ ...ransactionTransactionalSameMethodTest.java | 38 +++++ .../src/test/resources/application.properties | 5 + .../test/resources/initialTransactionData.sql | 5 + .../hibernate-reactive-transactions/pom.xml | 30 ++++ .../runtime/pom.xml | 61 +++++++ .../runtime/TransactionalInterceptorBase.java | 154 ++++++++++++++++++ .../TransactionalInterceptorMandatory.java | 19 +++ .../TransactionalInterceptorNever.java | 19 +++ .../TransactionalInterceptorNotSupported.java | 19 +++ .../TransactionalInterceptorRequired.java | 29 ++++ .../TransactionalInterceptorRequiresNew.java | 19 +++ .../TransactionalInterceptorSupports.java | 19 +++ .../resources/META-INF/quarkus-extension.yaml | 9 + .../reactive/deployment/ClassNames.java | 3 + .../HibernateReactiveCdiProcessor.java | 115 +++++++++---- .../runtime/HibernateReactiveRecorder.java | 74 +++++++++ .../reactive/runtime/OpenedSessionsState.java | 119 ++++++++++++++ ...uarkusReactiveConnectionPoolInitiator.java | 2 +- .../TransactionalContextConnection.java | 112 +++++++++++++ .../customized/TransactionalContextPool.java | 126 ++++++++++++++ .../TransactionalInterceptorBase.java | 13 +- .../TransactionalInterceptorMandatory.java | 4 + .../TransactionalInterceptorRequired.java | 5 + .../TransactionalInterceptorRequiresNew.java | 5 + .../runtime/AbstractUniInterceptor.java | 2 +- .../common/runtime/SessionOperations.java | 18 +- .../WithSessionOnDemandInterceptor.java | 1 + .../runtime/WithTransactionInterceptor.java | 14 ++ extensions/pom.xml | 1 + .../hibernate-reactive-transactions/pom.xml | 128 +++++++++++++++ ...HibernateReactiveTransactionsResource.java | 32 ++++ .../src/main/resources/application.properties | 0 ...bernateReactiveTransactionsResourceIT.java | 7 + ...rnateReactiveTransactionsResourceTest.java | 21 +++ integration-tests/pom.xml | 1 + pom.xml | 4 +- 56 files changed, 2120 insertions(+), 49 deletions(-) create mode 100644 extensions/hibernate-reactive-transactions/deployment/pom.xml create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/ConcurrencyTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/DisableJTATransactionTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/Hero.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveTransactionsTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/SupportOnlyRequiredTransactionTypeTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixReactiveBlockingTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixStatelessStatefulSessionTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandAndTransactionalSameMethodTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTransactionalSameMethodTest.java create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/resources/application.properties create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/resources/initialTransactionData.sql create mode 100644 extensions/hibernate-reactive-transactions/pom.xml create mode 100644 extensions/hibernate-reactive-transactions/runtime/pom.xml create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorMandatory.java create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNever.java create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNotSupported.java create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequired.java create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequiresNew.java create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorSupports.java create mode 100644 extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java create mode 100644 extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextConnection.java create mode 100644 extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java create mode 100644 integration-tests/hibernate-reactive-transactions/pom.xml create mode 100644 integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java create mode 100644 integration-tests/hibernate-reactive-transactions/src/main/resources/application.properties create mode 100644 integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceIT.java create mode 100644 integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 528a842d688bf..2bbdf2a5a930a 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1384,6 +1384,16 @@ quarkus-hibernate-reactive-deployment ${project.version} + + io.quarkus + hibernate-reactive-transactions + ${project.version} + + + io.quarkus + hibernate-reactive-transactions-deployment + ${project.version} + io.quarkus quarkus-panache-common diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java index 5c508ad6b746e..d4c2089367edf 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java @@ -14,6 +14,8 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.SessionFactory; +import org.hibernate.SharedSessionBuilder; +import org.hibernate.SharedStatelessSessionBuilder; import org.hibernate.StatelessSession; import org.hibernate.Transaction; import org.hibernate.graph.GraphSemantic; @@ -184,6 +186,16 @@ public Object getIdentifier(Object entity) { return delegate.get().getIdentifier(entity); } + @Override + public SharedStatelessSessionBuilder statelessWithOptions() { + return delegate.get().statelessWithOptions(); + } + + @Override + public SharedSessionBuilder sessionWithOptions() { + return delegate.get().sessionWithOptions(); + } + @Override public String getTenantIdentifier() { return delegate.get().getTenantIdentifier(); diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java index ea5c2a40032e1..d4d368dfc7491 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java @@ -14,7 +14,6 @@ import static org.hibernate.cfg.AvailableSettings.URL; import static org.hibernate.cfg.AvailableSettings.USER; import static org.hibernate.cfg.AvailableSettings.XML_MAPPING_ENABLED; -import static org.hibernate.internal.CoreLogging.messageLogger; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; @@ -98,7 +97,7 @@ public class FastBootMetadataBuilder { @Deprecated private static final String ALLOW_ENHANCEMENT_AS_PROXY = "hibernate.bytecode.allow_enhancement_as_proxy"; - private static final CoreMessageLogger LOG = messageLogger(FastBootMetadataBuilder.class); + private static final CoreMessageLogger LOG = CoreMessageLogger.CORE_LOGGER; // TODO Luca review this private final PersistenceUnitDescriptor persistenceUnit; private final BuildTimeSettings buildTimeSettings; @@ -268,7 +267,8 @@ private MergedSettings mergeSettings(QuarkusPersistenceUnitDefinition puDefiniti if (readBooleanConfigurationValue(cfg, AvailableSettings.FLUSH_BEFORE_COMPLETION)) { cfg.put(AvailableSettings.FLUSH_BEFORE_COMPLETION, "false"); - LOG.definingFlushBeforeCompletionIgnoredInHem(AvailableSettings.FLUSH_BEFORE_COMPLETION); + // TODO Luca review this + // LOG.definingFlushBeforeCompletionIgnoredInHem(AvailableSettings.FLUSH_BEFORE_COMPLETION); } // Quarkus specific @@ -607,7 +607,8 @@ private static void applyTransactionProperties(PersistenceUnitDescriptor persist } boolean hasTransactionStrategy = configurationValues.containsKey(TRANSACTION_COORDINATOR_STRATEGY); if (hasTransactionStrategy) { - LOG.overridingTransactionStrategyDangerous(TRANSACTION_COORDINATOR_STRATEGY); + // TODO Luca review this + // LOG.overridingTransactionStrategyDangerous(TRANSACTION_COORDINATOR_STRATEGY); } else { if (transactionType == PersistenceUnitTransactionType.JTA) { configurationValues.put(TRANSACTION_COORDINATOR_STRATEGY, diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusProxyFactory.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusProxyFactory.java index 9d5310e36e1ba..cb7a3fbac018d 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusProxyFactory.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/customized/QuarkusProxyFactory.java @@ -1,7 +1,5 @@ package io.quarkus.hibernate.orm.runtime.customized; -import static org.hibernate.internal.CoreLogging.messageLogger; - import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -28,7 +26,7 @@ */ public final class QuarkusProxyFactory implements ProxyFactory { - private static final CoreMessageLogger LOG = messageLogger(QuarkusProxyFactory.class); + private static final CoreMessageLogger LOG = CoreMessageLogger.CORE_LOGGER; // TODO Luca review this private final ProxyDefinitions proxyClassDefinitions; diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/FlatClassLoaderService.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/FlatClassLoaderService.java index a238a385b1d01..76d167e835ea1 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/FlatClassLoaderService.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/FlatClassLoaderService.java @@ -14,7 +14,6 @@ import org.hibernate.AssertionFailure; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; -import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; /** @@ -22,7 +21,7 @@ */ public class FlatClassLoaderService implements ClassLoaderService { - private static final CoreMessageLogger log = CoreLogging.messageLogger(FlatClassLoaderService.class); + private static final CoreMessageLogger log = CoreMessageLogger.CORE_LOGGER; // TODO Luca review this public static final ClassLoaderService INSTANCE = new FlatClassLoaderService(); private FlatClassLoaderService() { @@ -103,7 +102,8 @@ public Package packageForNameOrNull(String packageName) { Class aClass = Class.forName(packageName + ".package-info", false, getClassLoader()); return aClass == null ? null : aClass.getPackage(); } catch (ClassNotFoundException e) { - log.packageNotFound(packageName); + // TODO Luca review this + // log.packageNotFound(packageName); return null; } catch (LinkageError e) { log.warn("LinkageError while attempting to load Package named " + packageName, e); diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/QuarkusRuntimeInitDialectFactory.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/QuarkusRuntimeInitDialectFactory.java index 2b3d2ea6b1104..f185e13c40037 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/QuarkusRuntimeInitDialectFactory.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/service/QuarkusRuntimeInitDialectFactory.java @@ -1,7 +1,5 @@ package io.quarkus.hibernate.orm.runtime.service; -import static org.hibernate.internal.CoreLogging.messageLogger; - import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -26,7 +24,7 @@ * @see QuarkusStaticInitDialectFactory */ public class QuarkusRuntimeInitDialectFactory implements DialectFactory { - private static final CoreMessageLogger LOG = messageLogger(QuarkusRuntimeInitDialectFactory.class); + private static final CoreMessageLogger LOG = CoreMessageLogger.CORE_LOGGER; // TODO Luca review this private final String persistenceUnitName; private final boolean isFromPersistenceXml; private final Dialect dialect; diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java index 4be7b41fef6d7..ba946f68b5836 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedSession.java @@ -46,6 +46,7 @@ import org.hibernate.SessionEventListener; import org.hibernate.SessionFactory; import org.hibernate.SharedSessionBuilder; +import org.hibernate.SharedStatelessSessionBuilder; import org.hibernate.SimpleNaturalIdLoadAccess; import org.hibernate.Transaction; import org.hibernate.UnknownProfileException; @@ -672,6 +673,13 @@ public List> getEntityGraphs(Class entityClass) { } } + @Override + public SharedStatelessSessionBuilder statelessWithOptions() { + try (SessionResult emr = acquireSession()) { + return emr.session.statelessWithOptions(); + } + } + @Override public SharedSessionBuilder sessionWithOptions() { checkBlocking(); diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java index 27ec53a6e60a4..3aed7b61d1e04 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java @@ -19,6 +19,8 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.SessionFactory; +import org.hibernate.SharedSessionBuilder; +import org.hibernate.SharedStatelessSessionBuilder; import org.hibernate.StatelessSession; import org.hibernate.Transaction; import org.hibernate.graph.GraphSemantic; @@ -521,6 +523,20 @@ public void disableFilter(String filterName) { } } + @Override + public SharedStatelessSessionBuilder statelessWithOptions() { + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.statelessWithOptions(); + } + } + + @Override + public SharedSessionBuilder sessionWithOptions() { + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.sessionWithOptions(); + } + } + @Override public String getTenantIdentifier() { try (SessionResult emr = acquireSession()) { diff --git a/extensions/hibernate-reactive-transactions/deployment/pom.xml b/extensions/hibernate-reactive-transactions/deployment/pom.xml new file mode 100644 index 0000000000000..2c72265da81d6 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + + io.quarkus + hibernate-reactive-transactions-parent + 999-SNAPSHOT + + hibernate-reactive-transactions-deployment + Quarkus - Hibernate Reactive Transactions - Deployment + + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + hibernate-reactive-transactions + + + io.quarkus + quarkus-junit5-internal + test + + + io.quarkus + quarkus-hibernate-reactive-deployment + + + org.hibernate.reactive + hibernate-reactive-core + + + + io.smallrye.reactive + mutiny + + + + + + + io.quarkus + quarkus-test-vertx + test + + + org.assertj + assertj-core + test + + + io.quarkus + quarkus-reactive-pg-client-deployment + test + + + io.quarkus + quarkus-hibernate-reactive-panache-common-deployment + test + + + io.quarkus + quarkus-narayana-jta + test + + + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + + + diff --git a/extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java b/extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java new file mode 100644 index 0000000000000..7fc7f13c74303 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java @@ -0,0 +1,63 @@ +package io.quarkus.hibernate.reactive.transactions.deployment; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.runtime.configuration.ConfigurationException; + +class HibernateReactiveTransactionsProcessor { + + private static final String FEATURE = "hibernate-reactive-transactions"; + + private static final DotName TRANSACTIONAL = DotName.createSimple(Transactional.class.getName()); + + private static final String WITH_SESSION_ON_DEMAND = "io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand"; + private static final DotName WITH_SESSION = DotName + .createSimple("io.quarkus.hibernate.reactive.panache.common.WithSession"); + private static final DotName WITH_TRANSACTION = DotName + .createSimple("io.quarkus.hibernate.reactive.panache.common.WithTransaction"); + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @Inject + CombinedIndexBuildItem combinedIndexBuildItem; + + @BuildStep + void register( + BuildProducer reflectiveClass // TOOD Luca hack to make this build step run + ) { + + IndexView index = combinedIndexBuildItem.getIndex(); + + for (AnnotationInstance deserializeInstance : index.getAnnotations(TRANSACTIONAL)) { + AnnotationTarget annotationTarget = deserializeInstance.target(); + + if (annotationTarget.hasAnnotation(WITH_SESSION_ON_DEMAND)) { + throw new ConfigurationException("Cannot mix @Transactional and @WithSessionOnDemand"); + } + + if (annotationTarget.hasAnnotation(WITH_SESSION)) { + throw new ConfigurationException("Cannot mix @Transactional and @WithSession"); + } + + if (annotationTarget.hasAnnotation(WITH_TRANSACTION)) { + throw new ConfigurationException("Cannot mix @Transactional and @WithTransaction"); + } + + } + + } +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/ConcurrencyTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/ConcurrencyTest.java new file mode 100644 index 0000000000000..5b8a555067b78 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/ConcurrencyTest.java @@ -0,0 +1,92 @@ +package io.quarkus.hibernate.reactive.transactions.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Random; + +import jakarta.inject.Inject; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.groups.UniAndGroup2; + +public class ConcurrencyTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Hero.class) + .addAsResource("initialTransactionData.sql", "import.sql")) + .withConfigurationResource("application.properties"); + + @Inject + Mutiny.SessionFactory sessionFactory; + + @Test + @RunOnVertxContext + public void testCombineOperation(UniAsserter asserter) { + // initialTransactionData.sql + Long previousHeroId = 70L; + + Random rand = new Random(); + + int wait1 = rand.nextInt(1, 1000); + int wait2 = rand.nextInt(1, 1000); + + Uni doSomething1 = sessionFactory.withTransaction(session -> { + System.out.println("Start update 1 waiting " + wait1 + " threadId " + Thread.currentThread().getId()); + blockThread(wait1); + return updateHero(session, previousHeroId, "updatedName1") + .onItem().invoke(() -> System.out.println("End update 1 threadId " + Thread.currentThread().getId())); + + }); + + Uni doSomething2 = sessionFactory.withTransaction(session -> { + System.out.println("Start update 2 waiting " + wait2 + " threadId " + Thread.currentThread().getId()); + blockThread(wait2); + return updateHero(session, previousHeroId, "updatedName2").onItem() + .invoke(() -> System.out.println("End update 2 threadId " + Thread.currentThread().getId())); + }); + + UniAndGroup2 result = Uni.combine().all().unis( + doSomething1, + doSomething2); + + Uni refreshedHero = result.withUni((h1, h2) -> { + return null; + }).onFailure().recoverWithNull() + .chain(id -> sessionFactory.withTransaction(session -> findHero(session, previousHeroId))); + + asserter.assertThat(() -> refreshedHero, h -> { + assertThat(h.name).isEqualTo("updatedName2"); + }); + + } + + private static void blockThread(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public Uni updateHero(Mutiny.Session session, Long id, String newName) { + return session.find(Hero.class, id) + .map(h -> { + h.setName(newName); + return h; + }).call(() -> session.flush()); + } + + public Uni findHero(Mutiny.Session session, Long id) { + return session.find(Hero.class, id); + } + +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/DisableJTATransactionTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/DisableJTATransactionTest.java new file mode 100644 index 0000000000000..49d9f37f8f845 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/DisableJTATransactionTest.java @@ -0,0 +1,47 @@ +package io.quarkus.hibernate.reactive.transactions.test; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import jakarta.inject.Inject; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.Transactional; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; + +public class DisableJTATransactionTest { + + @Inject + TransactionManager transactionManager; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + @RunOnVertxContext + public void doNotCreateTransactionIfMethodIsAUni(UniAsserter asserter) throws SystemException { + asserter.assertThat(() -> transactionUniMethod(), h -> { + try { + assertNull(transactionManager.getTransaction()); + } catch (SystemException e) { + throw new RuntimeException(e); + } + }); + + } + + @Transactional(Transactional.TxType.REQUIRED) + Uni transactionUniMethod() { + return Uni.createFrom().item("transactionalUniMethod"); + } + +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/Hero.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/Hero.java new file mode 100644 index 0000000000000..c55e953b6e872 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/Hero.java @@ -0,0 +1,34 @@ +package io.quarkus.hibernate.reactive.transactions.test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity(name = "Hero") +@Table(name = "hero") +public class Hero { + + @Id + @GeneratedValue + public Long id; + + @Column + public String name; + + public Hero() { + } + + public Hero(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} \ No newline at end of file diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveTransactionsTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveTransactionsTest.java new file mode 100644 index 0000000000000..7f3897a9f947e --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveTransactionsTest.java @@ -0,0 +1,119 @@ +package io.quarkus.hibernate.reactive.transactions.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequired; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; + +public class HibernateReactiveTransactionsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar + .addClasses(Hero.class, TransactionalInterceptorRequired.class) + .addAsResource("initialTransactionData.sql", "import.sql")) + .withConfigurationResource("application.properties"); + + @Inject + Mutiny.SessionFactory sessionFactory; + + /** + * This test shows how to use hibernate reactive .withTransaction to set transactional boundaries + * Below there's testReactiveAnnotationTransaction which is the same test but with @Transactional + * + * @param asserter + */ + @Test + @RunOnVertxContext + public void testReactiveManualTransaction(UniAsserter asserter) { + // initialTransactionData.sql + Long heroId = 60L; + + // First update, make sure it's committed + asserter.assertThat( + () -> sessionFactory.withTransaction(session -> updateHero(session, heroId, "updatedNameCommitted")) + // 2nd endpoint call + .chain(() -> sessionFactory.withTransaction(session -> session.find(Hero.class, heroId))), + h -> assertThat(h.name).isEqualTo("updatedNameCommitted")); + + // Second update, make sure there's a rollback + asserter.assertThat( + () -> sessionFactory.withTransaction(session -> { + return updateHero(session, heroId, "this name won't appear") + .onItem().invoke(h -> { + throw new RuntimeException("Failing update"); + }); + }).onFailure().recoverWithNull() + .chain(() -> sessionFactory.withTransaction(session -> session.find(Hero.class, heroId))), + h -> { + assertThat(h.name).isEqualTo("updatedNameCommitted"); + }); + } + + @Inject + Mutiny.Session session; + + /* + * This is the same test as #testReactiveManualTransaction but instead of manually calling sessionFactory.withTransaction + * We use the annotation @Transactional + */ + @Test + @RunOnVertxContext + public void testReactiveAnnotationTransaction(UniAsserter asserter) { + // initialTransactionData.sql + Long heroId = 50L; + + // First update, make sure it's committed + asserter.assertThat( + () -> updateWithCommit(heroId, "updatedNameCommitted") + .chain(() -> findHero(heroId)), + h -> { + assertThat(h.name).isEqualTo("updatedNameCommitted"); + }); + + // Second update, make sure there's a rollback + asserter.assertThat( + () -> transactionalUpdateWithRollback(heroId, "this name won't appear") + .onFailure().recoverWithNull() + .chain(() -> findHero(heroId)), + h -> { + assertThat(h.name).isEqualTo("updatedNameCommitted"); + }); + } + + @Transactional + public Uni findHero(Long previousHeroId) { + return session.find(Hero.class, previousHeroId); + } + + @Transactional + public Uni updateWithCommit(Long previousHeroId, String newName) { + return updateHero(session, previousHeroId, newName); + } + + @Transactional + public Uni transactionalUpdateWithRollback(Long previousHeroId, String newName) { + return updateHero(session, previousHeroId, newName) + .onItem().invoke(h -> { + throw new RuntimeException("Failing update"); + }); + } + + public Uni updateHero(Mutiny.Session session, Long id, String newName) { + return session.find(Hero.class, id) + .map(h -> { + h.setName(newName); + return h; + }).call(() -> session.flush()); + } +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/SupportOnlyRequiredTransactionTypeTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/SupportOnlyRequiredTransactionTypeTest.java new file mode 100644 index 0000000000000..54ea037f72337 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/SupportOnlyRequiredTransactionTypeTest.java @@ -0,0 +1,91 @@ +package io.quarkus.hibernate.reactive.transactions.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.transaction.Transactional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorMandatory; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorNever; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorNotSupported; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequired; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequiresNew; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorSupports; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; + +public class SupportOnlyRequiredTransactionTypeTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addDefaultPackage() + // Every interceptor has to be added explictly for QuarkusUnitTest + .addClasses( + TransactionalInterceptorMandatory.class, + TransactionalInterceptorNever.class, + TransactionalInterceptorNotSupported.class, + TransactionalInterceptorRequired.class, + TransactionalInterceptorRequiresNew.class, + TransactionalInterceptorSupports.class)); + + private static final String ERROR_MESSAGE = "@Transactional on Reactive methods supports only Transactional.TxType.REQUIRED"; + + @Test + @RunOnVertxContext + public void testMandatory(UniAsserter asserter) { + asserter.assertFailedWith(() -> mandatory(), t -> assertThat(t).hasMessageContaining(ERROR_MESSAGE)); + } + + @Transactional(Transactional.TxType.MANDATORY) + public Uni mandatory() { + return Uni.createFrom().item("mandatory"); + } + + @Test + @RunOnVertxContext + public void testNever(UniAsserter asserter) { + asserter.assertFailedWith(() -> never(), t -> assertThat(t).hasMessageContaining(ERROR_MESSAGE)); + } + + @Transactional(Transactional.TxType.NEVER) + public Uni never() { + return Uni.createFrom().item("never"); + } + + @Test + @RunOnVertxContext + public void testNotSupported(UniAsserter asserter) { + asserter.assertFailedWith(() -> notSupported(), t -> assertThat(t).hasMessageContaining(ERROR_MESSAGE)); + } + + @Transactional(Transactional.TxType.NOT_SUPPORTED) + public Uni notSupported() { + return Uni.createFrom().item("not_supported"); + } + + @Test + @RunOnVertxContext + public void testRequiresNew(UniAsserter asserter) { + asserter.assertFailedWith(() -> requiresNew(), t -> assertThat(t).hasMessageContaining(ERROR_MESSAGE)); + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public Uni requiresNew() { + return Uni.createFrom().item("requiresNew"); + } + + @Test + @RunOnVertxContext + public void testSupports(UniAsserter asserter) { + asserter.assertFailedWith(() -> supports(), t -> assertThat(t).hasMessageContaining(ERROR_MESSAGE)); + } + + @Transactional(Transactional.TxType.SUPPORTS) + public Uni supports() { + return Uni.createFrom().item("supports"); + } +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixReactiveBlockingTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixReactiveBlockingTest.java new file mode 100644 index 0000000000000..ff0b6df0f3d43 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixReactiveBlockingTest.java @@ -0,0 +1,97 @@ +package io.quarkus.hibernate.reactive.transactions.test.mixing; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.Version; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequired; +import io.quarkus.hibernate.reactive.transactions.test.Hero; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.quarkus.vertx.VertxContextSupport; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; + +public class MixReactiveBlockingTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Hero.class, TransactionalInterceptorRequired.class)) + .setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-jdbc-postgresql-deployment", Version.getVersion()) // this triggers Agroal + )); + + @Inject + EntityManager entityManager; + + @Inject + Mutiny.SessionFactory reactiveSessionFactory; + + @Test + @RunOnVertxContext + @Disabled("WIP") + public void avoidMixingTransactionalAndWithTransactionTest(UniAsserter asserter) { + avoidMixingBlockingEMWithReactiveSessionFactory(); + // assert with Vertx + } + + @Transactional + public Uni avoidMixingBlockingEMWithReactiveSessionFactory() { + Hero heroBlocking = new Hero("heroName"); + Uni uni = Uni.createFrom() + .item(() -> { + // We're nesting @Transactional calls but we cannot reuse the same transaction + // As they're of two different kinds + // We're not sure this is testable, it might be in the blocking code we don't have the + // State of the context we need to check this + return blockingOp(heroBlocking); + }) + .runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + + return uni; + } + + @Transactional + public @Nullable Object blockingOp(Hero heroBlocking) { + entityManager.persist(heroBlocking); + return null; + } + + @Test + @Disabled("WIP") + public void avoidMixingTransactionalAndWithTransactionTest() throws Throwable { + Void v = fromBlockingToReactive(); + + // Assert blocking + } + + // https://quarkus.io/guides/vertx#executing-asynchronous-code-from-a-blocking-thread + @Transactional + public Void fromBlockingToReactive() throws Throwable { + Hero heroBlocking = new Hero("heroName"); + entityManager.persist(heroBlocking); + + Hero heroReactive = new Hero("heroName"); + + return VertxContextSupport.subscribeAndAwait(() -> { + return persistReactive(heroReactive); + }); + + } + + @Transactional + public Uni persistReactive(Hero heroReactive) { + return reactiveSessionFactory.withSession(session -> session.persist(heroReactive)); + } +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixStatelessStatefulSessionTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixStatelessStatefulSessionTest.java new file mode 100644 index 0000000000000..c5aa5598098f3 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixStatelessStatefulSessionTest.java @@ -0,0 +1,63 @@ +package io.quarkus.hibernate.reactive.transactions.test.mixing; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequired; +import io.quarkus.hibernate.reactive.transactions.test.Hero; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; + +public class MixStatelessStatefulSessionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(Hero.class, TransactionalInterceptorRequired.class)) + .withConfigurationResource("application.properties"); + + @Inject + Mutiny.SessionFactory mutinySessionFactory; + + @Test + @RunOnVertxContext + @Disabled("WIP") + public void avoidMixingTransactionalAndWithTransactionTest(UniAsserter asserter) { + Uni uni = avoidMixingDifferentSessionTypes(); + + asserter.assertFailedWith(() -> uni, + e -> assertThat(e.getCause()) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessage("Cannot mix Stateful and Stateless sessions")); + } + + @Transactional + public Uni avoidMixingDifferentSessionTypes() { + Hero heroStateful = new Hero("heroStateful"); + Hero heroStateless = new Hero("heroStateless"); + + // TODO this is an advanced scenario and we shoulnd't support this + + return mutinySessionFactory + .withSession(s -> { + return s.merge(heroStateful) + .flatMap(h -> s.flush()).onItem().invoke(() -> { + System.out.println("+++ First merge done"); + }); + }) + .flatMap(h1 -> { + return mutinySessionFactory.withStatelessSession( + s -> s.insert(heroStateless) + .onItem().invoke(() -> System.out.println("++++ Second insert done"))); + }); + } + +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandAndTransactionalSameMethodTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandAndTransactionalSameMethodTest.java new file mode 100644 index 0000000000000..1dc6282699f35 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandAndTransactionalSameMethodTest.java @@ -0,0 +1,38 @@ +package io.quarkus.hibernate.reactive.transactions.test.mixing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.transaction.Transactional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.smallrye.mutiny.Uni; + +public class MixWithSessionOnDemandAndTransactionalSameMethodTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addDefaultPackage()) + .assertException(throwable -> assertThat(throwable) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "Cannot mix @Transactional and @WithSession")); + + @Test + @RunOnVertxContext + public void avoidMixingTransactionalAndWithSessionTest() { + fail(); // this will never be called, extension will fail seeing the method below + } + + @Transactional + @WithSession + public Uni avoidMixingTransactionalAndWithSession() { + throw new UnsupportedOperationException("this shouldn't be called"); + } +} \ No newline at end of file diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandTest.java new file mode 100644 index 0000000000000..9895b78669c52 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandTest.java @@ -0,0 +1,65 @@ +package io.quarkus.hibernate.reactive.transactions.test.mixing; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.transaction.Transactional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequired; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; + +public class MixWithSessionOnDemandTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addDefaultPackage().addClass(TransactionalInterceptorRequired.class)); + + @Test + @RunOnVertxContext + public void testTransactionalCallingSessionOnDemand(UniAsserter asserter) { + asserter.assertFailedWith( + () -> methodAnnotatedWithTransactionalCallingSessionOnDemand(), + t -> assertThat(t) + .hasMessageContaining( + "Cannot call a method annotated with @WithSessionOnDemand from a method annotated with @Transactional")); + } + + @Transactional + public Uni methodAnnotatedWithTransactionalCallingSessionOnDemand() { + Uni a = Uni.createFrom().item("transactional_method"); + return a.flatMap(b -> methodAnnotatedWithSessionOnDemand()); + } + + @WithSessionOnDemand + public Uni methodAnnotatedWithSessionOnDemand() { + return Uni.createFrom().item("whatever"); + } + + @Test + @RunOnVertxContext + public void testSessionOnDemandCallingTransactional(UniAsserter asserter) { + // This should tell users do not do this and migrate to @Transactional + asserter.assertFailedWith( + () -> methodAnnotatedWithSessionOnDemandCallingTransactional(), + t -> assertThat(t) + .hasMessageContaining( + "Cannot call a method annotated with @Transactional from a method annotated with @WithSessionOnDemand")); + } + + @WithSessionOnDemand + public Uni methodAnnotatedWithSessionOnDemandCallingTransactional() { + Uni a = Uni.createFrom().item("with_session_on_demand_method"); + return a.flatMap(b -> methodAnnotatedWithTransactional()); + } + + @Transactional + public Uni methodAnnotatedWithTransactional() { + return Uni.createFrom().item("transactional_method"); + } +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTest.java new file mode 100644 index 0000000000000..061b13d866c7e --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTest.java @@ -0,0 +1,65 @@ +package io.quarkus.hibernate.reactive.transactions.test.mixing; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.transaction.Transactional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequired; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; + +public class MixWithTransactionTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addDefaultPackage().addClass(TransactionalInterceptorRequired.class)); + + @Test + @RunOnVertxContext + public void testTransactionalCallingWithTransaction(UniAsserter asserter) { + asserter.assertFailedWith( + () -> methodAnnotatedWithTransactionalCallingWithTransaction(), + t -> assertThat(t) + .hasMessageContaining( + "Cannot call a method annotated with @WithTransaction from a method annotated with @Transactional")); + } + + @Transactional + public Uni methodAnnotatedWithTransactionalCallingWithTransaction() { + Uni a = Uni.createFrom().item("transactional_method"); + return a.flatMap(b -> methodAnnotatedWithTransaction()); + } + + @WithTransaction + public Uni methodAnnotatedWithTransaction() { + return Uni.createFrom().item("with_transaction"); + } + + @Test + @RunOnVertxContext + public void testWithTransactionCallingTransactional(UniAsserter asserter) { + // This should tell users do not do this and migrate to @Transactional + asserter.assertFailedWith( + () -> methodAnnotatedWithTransactionCallingTransactional(), + t -> assertThat(t) + .hasMessageContaining( + "Cannot call a method annotated with @Transactional from a method annotated with @WithTransaction")); + } + + @WithTransaction + public Uni methodAnnotatedWithTransactionCallingTransactional() { + Uni a = Uni.createFrom().item("with_transaction"); + return a.flatMap(b -> methodAnnotatedWithTransactional()); + } + + @Transactional + public Uni methodAnnotatedWithTransactional() { + return Uni.createFrom().item("transactional_method"); + } +} diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTransactionalSameMethodTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTransactionalSameMethodTest.java new file mode 100644 index 0000000000000..85d94be655a13 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTransactionalSameMethodTest.java @@ -0,0 +1,38 @@ +package io.quarkus.hibernate.reactive.transactions.test.mixing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.transaction.Transactional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.smallrye.mutiny.Uni; + +public class MixWithTransactionTransactionalSameMethodTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addDefaultPackage()) + .assertException(throwable -> assertThat(throwable) + .isInstanceOf(ConfigurationException.class) + .hasMessageContaining( + "Cannot mix @Transactional and @WithTransaction")); + + @Test + @RunOnVertxContext + public void avoidMixingTransactionalAndWithTransactionTest() { + fail(); // this will never be called, extension will fail seeing the method below + } + + @Transactional + @WithTransaction + public Uni avoidMixingTransactionalAndWithTransaction() { + throw new UnsupportedOperationException("this shouldn't be called"); + } +} \ No newline at end of file diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/resources/application.properties b/extensions/hibernate-reactive-transactions/deployment/src/test/resources/application.properties new file mode 100644 index 0000000000000..7cb9929b40eab --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.datasource.db-kind=postgresql +quarkus.datasource.reactive=true + +quarkus.hibernate-orm.log.sql=true +quarkus.hibernate-orm.schema-management.strategy=drop-and-create diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/resources/initialTransactionData.sql b/extensions/hibernate-reactive-transactions/deployment/src/test/resources/initialTransactionData.sql new file mode 100644 index 0000000000000..334a22a4b3c45 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/resources/initialTransactionData.sql @@ -0,0 +1,5 @@ +insert into Hero (id, name) values (50, 'initialName'); + +insert into Hero (id, name) values (60, 'initialName'); + +insert into Hero (id, name) values (70, 'initialName'); diff --git a/extensions/hibernate-reactive-transactions/pom.xml b/extensions/hibernate-reactive-transactions/pom.xml new file mode 100644 index 0000000000000..868c199d7f51a --- /dev/null +++ b/extensions/hibernate-reactive-transactions/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + io.quarkus + quarkus-extensions-parent + 999-SNAPSHOT + + hibernate-reactive-transactions-parent + pom + Quarkus - Hibernate Reactive Transactions - Parent + + + deployment + runtime + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + + + + + diff --git a/extensions/hibernate-reactive-transactions/runtime/pom.xml b/extensions/hibernate-reactive-transactions/runtime/pom.xml new file mode 100644 index 0000000000000..1ad6a11aa6f30 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + io.quarkus + hibernate-reactive-transactions-parent + 999-SNAPSHOT + + hibernate-reactive-transactions + Quarkus - Hibernate Reactive Transactions - Runtime + Quarkus - Hibernate Reactive Transaction manager + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-hibernate-reactive + + + + + + + io.quarkus + quarkus-extension-maven-plugin + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + + + true + + + + + + + diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java new file mode 100644 index 0000000000000..1fdc10d0c4b15 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java @@ -0,0 +1,154 @@ +package io.quarkus.hibernate.reactive.transactions.runtime; + +import static io.quarkus.hibernate.reactive.runtime.HibernateReactiveRecorder.WITH_TRANSACTION_METHOD_KEY; +import static io.quarkus.hibernate.reactive.runtime.customized.TransactionalContextPool.CURRENT_TRANSACTION_KEY; + +import java.util.Optional; +import java.util.function.Supplier; + +import jakarta.interceptor.InvocationContext; +import jakarta.transaction.Transactional; + +import org.jboss.logging.Logger; + +import io.quarkus.hibernate.reactive.runtime.HibernateReactiveRecorder; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; +import io.vertx.sqlclient.Transaction; + +/** + * The base intereceptor which manages reactive transactions for methods + * annotated with {@link Transactional}. + * Each value has its own class as the QuarkusReactiveTransaction Type is binding so requires exact match + */ +public abstract class TransactionalInterceptorBase { + + private static final Logger LOG = Logger.getLogger(TransactionalInterceptorBase.class); + + private static final String ERROR_MSG = "Hibernate Reactive Panache requires a safe (isolated) Vert.x sub-context, but the current context hasn't been flagged as such."; + + public Object intercept(InvocationContext context) throws Exception { + if (isUniReturnType(context)) { + Optional> typeValidation = validateTransactionalType(context); + + if (typeValidation.isPresent()) { + return typeValidation.get(); + } + + return withTransactionalSessionOnDemand(() -> { + // Handle checked exception vs runtime exception differently according to the spec + // check blicking interceptor for java.lang.Error as well + // copy the logic from io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java:363 + return proceedUni(context); + }).onFailure().call(this::rollback) + .onCancellation().call(this::rollback) + .call(this::commit); + } + return context.proceed(); + } + + Transaction transaction() { + return Vertx.currentContext().getLocal(CURRENT_TRANSACTION_KEY); + } + + // Copied from org/hibernate/reactive/pool/impl/SqlClientConnection.java:305 + Uni commit() { + Transaction transaction = transaction(); + return Uni.createFrom().completionStage(transaction.commit() + .onSuccess(v -> LOG.tracef("Transaction committed: %s", transaction)) + .onFailure(v -> LOG.tracef("Failed to commit transaction: %s", transaction)) + .toCompletionStage()); + } + + // Copied from org/hibernate/reactive/pool/impl/SqlClientConnection.java:314 + Uni rollback() { + Transaction transaction = transaction(); + return Uni.createFrom().completionStage(transaction.rollback() + .onFailure(v -> LOG.tracef("Failed to rollback transaction: %s", transaction)) + .onSuccess(v -> LOG.tracef("Transaction rolled back: %s", transaction)) + .toCompletionStage()); + } + + // TODO copied from Panache -- refactor and put in a common module? + @SuppressWarnings("unchecked") + protected Uni proceedUni(InvocationContext context) { + try { + return ((Uni) context.proceed()); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + } + + // TODO copied from Panache -- refactor and put in a common module? + protected boolean isUniReturnType(InvocationContext context) { + return context.getMethod().getReturnType().equals(Uni.class); + } + + // This key is used to indicate the method was annotated with @Transactional + // And will open a session and a transaction lazy when the first operation requrires a reactive session + // Check HibernateReactiveRecorder.sessionSupplier to see where the session is injected + // TODO Luca find a way to remove the duplication between this field and TransactionalInterceptor field + private static final String TRANSACTIONAL_METHOD_KEY = "hibernate.reactive.methodTransactional"; + + // This key is copied from panache and it's the marker key the WithSessionOnDemand intereceptor uses + private static final String SESSION_ON_DEMAND_KEY = "hibernate.reactive.panache.sessionOnDemand"; + + static Uni withTransactionalSessionOnDemand(Supplier> work) { + Context context = vertxContext(); + if (context.getLocal(SESSION_ON_DEMAND_KEY) != null) { + return Uni.createFrom().failure( + new UnsupportedOperationException( + "Cannot call a method annotated with @Transactional from a method annotated with @WithSessionOnDemand")); + } + + if (context.getLocal(WITH_TRANSACTION_METHOD_KEY) != null) { + return Uni.createFrom().failure( + new UnsupportedOperationException( + "Cannot call a method annotated with @Transactional from a method annotated with @WithTransaction")); + } + + // TODO check that there's no other session opened by session delegators for another PU + + // TODO check that there's no statelessSession opened by statelessSession delegators + + // io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java:79 + if (context.getLocal(TRANSACTIONAL_METHOD_KEY) != null) { + return work.get(); + } else { + // mark this method to be @Transactional so that other Panache interceptor might fail + context.putLocal(TRANSACTIONAL_METHOD_KEY, true); + // perform the work and eventually close the session and remove the key + return work.get().eventually(() -> { + context.removeLocal(TRANSACTIONAL_METHOD_KEY); + return HibernateReactiveRecorder.OPENED_SESSIONS_STATE.closeAllOpenedSessions(context); + }); + } + } + + /** + * + * @return the current vertx duplicated context + * @throws IllegalStateException If no vertx context is found or is not a safe context as mandated by the + * {@link VertxContextSafetyToggle} + */ + private static Context vertxContext() { + Context context = Vertx.currentContext(); + if (context != null) { + VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); + return context; + } else { + throw new IllegalStateException("No current Vertx context found"); + } + } + + protected Optional> validateTransactionalType(InvocationContext context) { + Transactional transactional = context.getMethod().getAnnotation(Transactional.class); + if (transactional != null && transactional.value() != Transactional.TxType.REQUIRED) { + return Optional.of(Uni.createFrom().failure(new UnsupportedOperationException( + "@Transactional on Reactive methods supports only Transactional.TxType.REQUIRED"))); + } + return Optional.empty(); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorMandatory.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorMandatory.java new file mode 100644 index 0000000000000..117a7f51749b6 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorMandatory.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.reactive.transactions.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.transaction.Transactional; + +@Transactional(Transactional.TxType.MANDATORY) +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 300) +public class TransactionalInterceptorMandatory extends TransactionalInterceptorBase { + + @Override + @AroundInvoke + public Object intercept(InvocationContext ic) throws Exception { + return super.intercept(ic); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNever.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNever.java new file mode 100644 index 0000000000000..34afdd2be4414 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNever.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.reactive.transactions.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.transaction.Transactional; + +@Transactional(Transactional.TxType.NEVER) +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 300) +public class TransactionalInterceptorNever extends TransactionalInterceptorBase { + + @Override + @AroundInvoke + public Object intercept(InvocationContext ic) throws Exception { + return super.intercept(ic); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNotSupported.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNotSupported.java new file mode 100644 index 0000000000000..b1cbd0e9059a1 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorNotSupported.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.reactive.transactions.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.transaction.Transactional; + +@Transactional(Transactional.TxType.NOT_SUPPORTED) +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 300) +public class TransactionalInterceptorNotSupported extends TransactionalInterceptorBase { + + @Override + @AroundInvoke + public Object intercept(InvocationContext ic) throws Exception { + return super.intercept(ic); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequired.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequired.java new file mode 100644 index 0000000000000..57eb07be61703 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequired.java @@ -0,0 +1,29 @@ +package io.quarkus.hibernate.reactive.transactions.runtime; + +import java.util.Optional; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.transaction.Transactional; + +import io.smallrye.mutiny.Uni; + +@Transactional(Transactional.TxType.REQUIRED) +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 300) +public class TransactionalInterceptorRequired extends TransactionalInterceptorBase { + + @Override + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + return super.intercept(context); + } + + @Override + protected Optional> validateTransactionalType(InvocationContext context) { + // Required is supported + return Optional.empty(); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequiresNew.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequiresNew.java new file mode 100644 index 0000000000000..025d7fe8251df --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorRequiresNew.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.reactive.transactions.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.transaction.Transactional; + +@Transactional(Transactional.TxType.REQUIRES_NEW) +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 300) +public class TransactionalInterceptorRequiresNew extends TransactionalInterceptorBase { + + @Override + @AroundInvoke + public Object intercept(InvocationContext ic) throws Exception { + return super.intercept(ic); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorSupports.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorSupports.java new file mode 100644 index 0000000000000..6a2a1c7522ebb --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorSupports.java @@ -0,0 +1,19 @@ +package io.quarkus.hibernate.reactive.transactions.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import jakarta.transaction.Transactional; + +@Transactional(Transactional.TxType.SUPPORTS) +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 300) +public class TransactionalInterceptorSupports extends TransactionalInterceptorBase { + + @Override + @AroundInvoke + public Object intercept(InvocationContext ic) throws Exception { + return super.intercept(ic); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..c9fa622b4b3de --- /dev/null +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: Quarkus - Hibernate Reactive Transactions +#description: Quarkus - Hibernate Reactive Transaction manager +metadata: +# keywords: +# - hibernate-reactive-transactions +# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension +# categories: +# - "miscellaneous" +# status: "preview" diff --git a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/ClassNames.java b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/ClassNames.java index 726fd51258984..b2c6228502b51 100644 --- a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/ClassNames.java +++ b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/ClassNames.java @@ -17,6 +17,9 @@ private static DotName createConstant(String fqcn) { return result; } + public static final DotName MUTINY_SESSION = createConstant("org.hibernate.reactive.mutiny.Mutiny$Session"); + public static final DotName MUTINY_STATELESS_SESSION = createConstant( + "org.hibernate.reactive.mutiny.Mutiny$StatelessSession"); public static final DotName MUTINY_SESSION_FACTORY = createConstant("org.hibernate.reactive.mutiny.Mutiny$SessionFactory"); public static final DotName IMPLEMENTOR = createConstant("org.hibernate.reactive.common.spi.Implementor"); } diff --git a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveCdiProcessor.java b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveCdiProcessor.java index c9452066f5b83..4f3c0a5ea1547 100644 --- a/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveCdiProcessor.java +++ b/extensions/hibernate-reactive/deployment/src/main/java/io/quarkus/hibernate/reactive/deployment/HibernateReactiveCdiProcessor.java @@ -1,10 +1,13 @@ package io.quarkus.hibernate.reactive.deployment; import static io.quarkus.hibernate.reactive.deployment.ClassNames.IMPLEMENTOR; +import static io.quarkus.hibernate.reactive.deployment.ClassNames.MUTINY_SESSION; import static io.quarkus.hibernate.reactive.deployment.ClassNames.MUTINY_SESSION_FACTORY; +import static io.quarkus.hibernate.reactive.deployment.ClassNames.MUTINY_STATELESS_SESSION; import java.util.Arrays; import java.util.List; +import java.util.function.Supplier; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Default; @@ -13,12 +16,14 @@ import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import io.quarkus.arc.ActiveResult; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; -import io.quarkus.hibernate.orm.deployment.ClassNames; +import io.quarkus.hibernate.orm.PersistenceUnit; import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem; import io.quarkus.hibernate.orm.runtime.JPAConfig; import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; @@ -29,9 +34,15 @@ public class HibernateReactiveCdiProcessor { private static final List MUTINY_SESSION_FACTORY_EXPOSED_TYPES = Arrays.asList( MUTINY_SESSION_FACTORY, IMPLEMENTOR); + private static final List MUTINY_SESSION_EXPOSED_TYPES = Arrays.asList( + MUTINY_SESSION); + + private static final List MUTINY_STATELESS_SESSION_EXPOSED_TYPES = Arrays.asList( + MUTINY_STATELESS_SESSION); + @Record(ExecutionTime.RUNTIME_INIT) @BuildStep - void produceSessionFactoryBean( + void registerBeans( HibernateReactiveRecorder recorder, List persistenceUnitDescriptors, BuildProducer syntheticBeanBuildItemBuildProducer) { @@ -46,39 +57,87 @@ void produceSessionFactoryBean( boolean isReactive = persistenceUnitDescriptor.isReactive(); if (isReactive) { - SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem - .configure(Mutiny.SessionFactory.class) - // NOTE: this is using ApplicationScope and not Singleton, by design, in order to be mockable - // See https://github.com/quarkusio/quarkus/issues/16437 - .scope(ApplicationScoped.class) - .unremovable() - .setRuntimeInit() - // Note persistence units _actually_ get started a bit earlier, each in its own thread. See JPAConfig#startAll. - // This startup() call is only necessary in order to trigger Arc's usage checks (fail startup if bean injected when a PU is inactive). - .startup() - .checkActive(recorder.checkActiveSupplier(persistenceUnitName, + PersistenceUnitReference puRef = new PersistenceUnitReference( + persistenceUnitName, + recorder.checkActiveSupplier( + persistenceUnitDescriptor.getPersistenceUnitName(), persistenceUnitDescriptor.getConfig().getDataSource(), - persistenceUnitDescriptor.getConfig().getEntityClassNames())) - .createWith(recorder.mutinySessionFactory(persistenceUnitName)) - .addInjectionPoint(ClassType.create(DotName.createSimple(JPAConfig.class))); + persistenceUnitDescriptor.getConfig().getEntityClassNames())); + + produceSessionFactoryBean(syntheticBeanBuildItemBuildProducer, recorder, puRef); + + produceSessionBeans(syntheticBeanBuildItemBuildProducer, recorder, puRef); + } - for (DotName exposedType : MUTINY_SESSION_FACTORY_EXPOSED_TYPES) { - configurator.addType(exposedType); - } + } + } - configurator.defaultBean(); + private void produceSessionFactoryBean( + BuildProducer producer, + HibernateReactiveRecorder recorder, + PersistenceUnitReference puRef) { - if (isDefaultPU) { - configurator.addQualifier(Default.class); - } else { - configurator.addQualifier().annotation(ClassNames.QUARKUS_PERSISTENCE_UNIT) - .addValue("value", persistenceUnitName).done(); - } + producer.produce(createSyntheticBean(puRef, + Mutiny.SessionFactory.class, MUTINY_SESSION_FACTORY_EXPOSED_TYPES, true) + .createWith(recorder.mutinySessionFactory(puRef.persistenceUnitName)) + .addInjectionPoint(ClassType.create(DotName.createSimple(JPAConfig.class))) + .done()); + } - syntheticBeanBuildItemBuildProducer.produce(configurator.done()); - } + private void produceSessionBeans( + BuildProducer producer, + HibernateReactiveRecorder recorder, + PersistenceUnitReference puRef) { + + // Create Session bean + producer.produce(createSyntheticBean(puRef, + Mutiny.Session.class, MUTINY_SESSION_EXPOSED_TYPES, false) + .createWith(recorder.sessionSupplier(puRef.persistenceUnitName)) + .done()); + + // Create StatelessSession bean + producer.produce(createSyntheticBean(puRef, + Mutiny.StatelessSession.class, MUTINY_STATELESS_SESSION_EXPOSED_TYPES, false) + .createWith(recorder.statelessSessionSupplier(puRef.persistenceUnitName)) + .done()); + } + + private static SyntheticBeanBuildItem.ExtendedBeanConfigurator createSyntheticBean(PersistenceUnitReference puRef, + Class type, List allExposedTypes, boolean defaultBean) { + SyntheticBeanBuildItem.ExtendedBeanConfigurator configurator = SyntheticBeanBuildItem + .configure(type) + // NOTE: this is using ApplicationScope and not Singleton, by design, in order to be mockable + // See https://github.com/quarkusio/quarkus/issues/16437 + .scope(ApplicationScoped.class) + .unremovable() + .setRuntimeInit() + // Note persistence units _actually_ get started a bit earlier, each in its own thread. See JPAConfig#startAll. + // This startup() call is only necessary in order to trigger Arc's usage checks (fail startup if bean injected when a PU is inactive). + .startup() + .checkActive(puRef.checkActiveSupplier); + + for (DotName exposedType : allExposedTypes) { + configurator.addType(exposedType); + } + + if (defaultBean) { + configurator.defaultBean(); + } + boolean defaultPuName = PersistenceUnitUtil.isDefaultPersistenceUnit(puRef.persistenceUnitName); + if (defaultPuName) { + configurator.addQualifier(Default.class); } + + configurator.addQualifier().annotation(DotNames.NAMED).addValue("value", puRef.persistenceUnitName).done(); + configurator.addQualifier().annotation(PersistenceUnit.class).addValue("value", puRef.persistenceUnitName).done(); + + return configurator; + } + + private record PersistenceUnitReference( + String persistenceUnitName, + Supplier checkActiveSupplier) { } } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java index d9be45552ec03..4bafdda79e16f 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java @@ -10,6 +10,8 @@ import org.hibernate.SessionFactory; import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.delegation.MutinySessionDelegator; +import org.hibernate.reactive.mutiny.delegation.MutinyStatelessSessionDelegator; import io.quarkus.arc.ActiveResult; import io.quarkus.arc.SyntheticCreationalContext; @@ -20,6 +22,8 @@ import io.quarkus.reactive.datasource.runtime.ReactiveDataSourceUtil; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Context; +import io.vertx.core.Vertx; @Recorder public class HibernateReactiveRecorder { @@ -29,6 +33,8 @@ public HibernateReactiveRecorder(final RuntimeValue r this.runtimeConfig = runtimeConfig; } + public static final OpenedSessionsState OPENED_SESSIONS_STATE = new OpenedSessionsState(); + /** * The feature needs to be initialized, even if it's not enabled. * @@ -90,4 +96,72 @@ public Mutiny.SessionFactory apply(SyntheticCreationalContext, Mutiny.Session> sessionSupplier(String persistenceUnitName) { + return new Function, Mutiny.Session>() { + + @Override + public Mutiny.Session apply(SyntheticCreationalContext context) { + return new MutinySessionDelegator() { + @Override + public Mutiny.Session delegate() { + + // TODO check we're in @Transactional + // TODO get the session from vert.x context or open it (similar to Panache.getSession) + // To open, use SessionFactory#openSessionWithLazyConnectionOpening -> returns Mutiny.Session + + return getSession(persistenceUnitName); + } + }; + } + }; + } + + // This key is used to indicate the method was annotated with @Transactional + // And will open a session and a transaction lazy when the first operation requrires a reactive session + // Check HibernateReactiveRecorder.sessionSupplier to see where the session is injected + // TODO Luca find a way to remove the duplication between this field and TransactionalInterceptor TRANSACTIONAL_METHOD_KEY field + public static final String TRANSACTIONAL_METHOD_KEY = "hibernate.reactive.methodTransactional"; + + // TODO Luca find a way to remove duplication + public static final String WITH_TRANSACTION_METHOD_KEY = "hibernate.reactive.withTransaction"; + + public static Mutiny.Session getSession(String persistenceUnitName) { + Context context = Vertx.currentContext(); + + Optional openedSession = OPENED_SESSIONS_STATE.getOpenedSession(context, + persistenceUnitName); + // reuse the existing reactive session + if (openedSession.isPresent()) { + return openedSession.get().session(); + } else if (context.getLocal(TRANSACTIONAL_METHOD_KEY) == null) { + throw new IllegalStateException("No current Mutiny.Session found" + + "\n\t- no reactive session was found in the Vert.x context and the context was not marked to open a new session lazily" + + "\n\t- a session is opened automatically for JAX-RS resource methods annotated with an HTTP method (@GET, @POST, etc.); inherited annotations are not taken into account" + + "\n\t- you may need to annotate the business method with @Transactional"); + } else { + return OPENED_SESSIONS_STATE.createNewSession(persistenceUnitName, context); + } + } + + public Function, Mutiny.StatelessSession> statelessSessionSupplier( + String persistenceUnitName) { + return new Function, Mutiny.StatelessSession>() { + + @Override + public Mutiny.StatelessSession apply(SyntheticCreationalContext context) { + return new MutinyStatelessSessionDelegator() { + @Override + public Mutiny.StatelessSession delegate() { + // TODO check we're in @Transactional + // TODO get the session from vert.x context or open it (similar to Panache.getSession) + // To open, use SessionFactory#openSessionWithLazyConnectionOpening -> returns Mutiny.Session + + throw new UnsupportedOperationException(); + + } + }; + } + }; + } + } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java new file mode 100644 index 0000000000000..5ce0ee32cb46c --- /dev/null +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java @@ -0,0 +1,119 @@ +package io.quarkus.hibernate.reactive.runtime; + +import static io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.hibernate.reactive.common.spi.Implementor; +import org.hibernate.reactive.context.Context.Key; +import org.hibernate.reactive.context.impl.BaseKey; +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.impl.MutinySessionImpl; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ClientProxy; +import io.quarkus.arc.impl.ComputingCache; +import io.quarkus.hibernate.orm.PersistenceUnit; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; + +public class OpenedSessionsState { + // This key is used to keep track of the Set sessions created on demand + private static final String SESSIONS_ON_DEMAND_OPENED_KEY = "hibernate.reactive.panache.sessionOnDemandOpened"; + + private final ComputingCache> sessionKeys = new ComputingCache<>( + k -> createSessionFactoryAndStoreKey(k)); + + private final ComputingCache sessionFactories = new ComputingCache<>( + k -> createSessionFactory(k)); + + public record SessionWithKey(org.hibernate.reactive.context.Context.Key key, Mutiny.Session session) { + + } + + public Optional getOpenedSession(Context context, String persistenceUnitName) { + Key sessionKey = sessionKeys.getValue(persistenceUnitName); + return getOpenedSession(context, sessionKey); + } + + private static Optional getOpenedSession(Context context, Key sessionKey) { + Mutiny.Session current = context.getLocal(sessionKey); + return Optional.ofNullable(current) + .filter(s -> s.isOpen()) + .map(s -> new SessionWithKey(sessionKey, s)); + } + + public Uni closeAllOpenedSessions(Context context) { + Set onDemandSessionCreated = openedSessionContextSet(context); + if (onDemandSessionCreated.isEmpty()) { + return Uni.createFrom().voidItem(); + } + List> closedSessionsUnis = new ArrayList<>(); + for (String s : onDemandSessionCreated) { + Optional openedSession = getOpenedSession(context, s); + closedSessionsUnis.add(closeAndRemoveSession(context, openedSession)); + } + context.removeLocal(SESSIONS_ON_DEMAND_OPENED_KEY); + return Uni.combine().all().unis(closedSessionsUnis).discardItems(); + } + + public Mutiny.Session createNewSession(String persistenceUnitName, Context context) { + + Set openedSession = openedSessionContextSet(context); + + // open a new reactive session and store it in the vertx duplicated context + // the context was marked as "lazy" which means that the session will be eventually closed + openedSession.add(persistenceUnitName); + + Mutiny.SessionFactory sessionFactory = sessionFactories.getValue(persistenceUnitName); + MutinySessionImpl session = (MutinySessionImpl) sessionFactory.createSession(); + + Key sessionKey = sessionKeys.getValue(persistenceUnitName); + context.putLocal(sessionKey, session); + + return session; + } + + private Set openedSessionContextSet(Context context) { + // This will keep track of all on-demand opened sessions + Set onDemandSessionsCreated = context.getLocal(SESSIONS_ON_DEMAND_OPENED_KEY); + if (onDemandSessionsCreated == null) { + onDemandSessionsCreated = new HashSet<>(); + context.putLocal(SESSIONS_ON_DEMAND_OPENED_KEY, onDemandSessionsCreated); + } + return onDemandSessionsCreated; + } + + private Uni closeAndRemoveSession(Context context, Optional openSession) { + return openSession.map((SessionWithKey s) -> s.session.close() + .eventually(() -> context.removeLocal(s.key))) + .orElse(Uni.createFrom().voidItem()); + } + + private Key createSessionFactoryAndStoreKey(String persistenceUnitName) { + Mutiny.SessionFactory value = createSessionFactory(persistenceUnitName); + Implementor implementor = (Implementor) ClientProxy.unwrap(value); + return new BaseKey<>(Mutiny.Session.class, implementor.getUuid()); + } + + private Mutiny.SessionFactory createSessionFactory(String persistenceunitname) { + Mutiny.SessionFactory sessionFactory; + + // Note that Mutiny.SessionFactory is @ApplicationScoped bean - it's safe to use the cached client proxy + if (DEFAULT_PERSISTENCE_UNIT_NAME.equals(persistenceunitname)) { + sessionFactory = Arc.container().instance(Mutiny.SessionFactory.class).get(); + } else { + sessionFactory = Arc.container().instance(Mutiny.SessionFactory.class, + new PersistenceUnit.PersistenceUnitLiteral(persistenceunitname)).get(); + } + + if (sessionFactory == null) { + throw new IllegalStateException("Mutiny.SessionFactory bean not found"); + } + return sessionFactory; + } +} diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java index d7bfdd9cdaddc..7badd1199c1f6 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/QuarkusReactiveConnectionPoolInitiator.java @@ -31,7 +31,7 @@ public ReactiveConnectionPool initiateService(Map configurationValues, ServiceRe // nothing to do, but given the separate hierarchies have to handle this here. return null; } - return new QuarkusSqlClientPool(pool); + return new QuarkusSqlClientPool(new TransactionalContextPool(pool)); } } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextConnection.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextConnection.java new file mode 100644 index 0000000000000..aebd76f5c73ec --- /dev/null +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextConnection.java @@ -0,0 +1,112 @@ +package io.quarkus.hibernate.reactive.runtime.customized; + +import io.vertx.codegen.annotations.Fluent; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.sqlclient.PrepareOptions; +import io.vertx.sqlclient.PreparedQuery; +import io.vertx.sqlclient.PreparedStatement; +import io.vertx.sqlclient.Query; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.SqlConnection; +import io.vertx.sqlclient.Transaction; +import io.vertx.sqlclient.spi.DatabaseMetadata; + +public class TransactionalContextConnection implements SqlConnection { + private final SqlConnection connection; + + public TransactionalContextConnection(SqlConnection connection) { + this.connection = connection; + } + + @Fluent + @Override + public SqlConnection prepare(String sql, Handler> handler) { + return connection.prepare(sql, handler); + } + + @Override + public Future prepare(String sql) { + return connection.prepare(sql); + } + + @Fluent + @Override + public SqlConnection prepare(String sql, PrepareOptions options, Handler> handler) { + return connection.prepare(sql, options, handler); + } + + @Override + public Future prepare(String sql, PrepareOptions options) { + return connection.prepare(sql, options); + } + + @Fluent + @Override + public SqlConnection exceptionHandler(Handler handler) { + return connection.exceptionHandler(handler); + } + + @Fluent + @Override + public SqlConnection closeHandler(Handler handler) { + return connection.closeHandler(handler); + } + + @Override + public void begin(Handler> handler) { + connection.begin(handler); + } + + @Override + public Future begin() { + return connection.begin(); + } + + @Override + public Transaction transaction() { + return connection.transaction(); + } + + @Override + public boolean isSSL() { + return connection.isSSL(); + } + + @Override + public void close(Handler> handler) { + connection.close(handler); + } + + @Override + public DatabaseMetadata databaseMetadata() { + return connection.databaseMetadata(); + } + + @Override + public Query> query(String sql) { + return connection.query(sql); + } + + @Override + public PreparedQuery> preparedQuery(String sql) { + return connection.preparedQuery(sql); + } + + @Override + public PreparedQuery> preparedQuery(String sql, PrepareOptions options) { + return connection.preparedQuery(sql, options); + } + + @Override + public Future close() { + + // Do not actually close this connection as the TransactionalInterceptor should commit the tx first + + // return connection.close(); + // + return Future.succeededFuture(); + } +} diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java new file mode 100644 index 0000000000000..73461e0ef16c3 --- /dev/null +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java @@ -0,0 +1,126 @@ +package io.quarkus.hibernate.reactive.runtime.customized; + +import static io.quarkus.hibernate.reactive.runtime.HibernateReactiveRecorder.TRANSACTIONAL_METHOD_KEY; + +import java.util.function.Function; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.impl.ContextInternal; +import io.vertx.sqlclient.Pool; +import io.vertx.sqlclient.PrepareOptions; +import io.vertx.sqlclient.PreparedQuery; +import io.vertx.sqlclient.Query; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.SqlConnection; + +/** + * A pool that handles transaction based on Vert.x context set by the @Transactional interceptor. + */ +public class TransactionalContextPool implements Pool { + + // Used in this class and in TransactionalInterceptor to get the lazily created Transaction + public static final String CURRENT_TRANSACTION_KEY = "hibernate.reactive.currentTransaction"; + + private final Pool delegate; + + public TransactionalContextPool(Pool delegate) { + this.delegate = delegate; + } + + @Override + public void getConnection(Handler> handler) { + if (!shouldOpenTransaction()) { + delegate.getConnection(handler); + } else { + delegate.getConnection(result -> { + if (result.failed()) { + handler.handle(result); + } + var connection = result.result(); + connection.begin() + // Ignore the returned transaction; the caller expects a SqlConnection, + // and the Transaction can be accessed through connection.transaction() anyway. + .map(ignored -> connection) + .andThen(handler); + }); + } + } + + @Override + public Future getConnection() { + if (!shouldOpenTransaction()) { + return delegate.getConnection(); + } else { + return delegate.getConnection() + .compose(connection -> { + return connection.begin().map(t -> { + Vertx.currentContext().putLocal(CURRENT_TRANSACTION_KEY, connection.transaction()); + return new TransactionalContextConnection(connection); + }); + }); + } + } + + private boolean shouldOpenTransaction() { + + Context context = Vertx.currentContext(); + + // Vert.x context during DB Validation in startup is null + // When using reactive in a @Transactional method, the context is surely duplicated + if (context != null && ((ContextInternal) context).isDuplicate()) { + Object createTransaction = context.getLocal(TRANSACTIONAL_METHOD_KEY); + return createTransaction != null && (boolean) createTransaction; + } else { + return false; + } + } + + @Override + public Query> query(String sql) { + return delegate.query(sql); + } + + @Override + public PreparedQuery> preparedQuery(String sql) { + return delegate.preparedQuery(sql); + } + + @Override + public void close(Handler> handler) { + delegate.close(handler); + } + + @Override + @Deprecated + public Pool connectHandler(Handler handler) { + // Deprecated, and not needed by Reactive, and no idea how it affects auto-transactions. + throw new UnsupportedOperationException("This operation is not supported"); + } + + @Override + @Deprecated + public Pool connectionProvider(Function> provider) { + // Deprecated, and not needed by Reactive, and no idea how it affects auto-transactions. + throw new UnsupportedOperationException("This operation is not supported"); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public PreparedQuery> preparedQuery(String sql, PrepareOptions options) { + return delegate.preparedQuery(sql, options); + } + + @Override + public Future close() { + return delegate.close(); + } +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java index d965b3b1da999..1328e9ed15dfd 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java @@ -32,6 +32,7 @@ import io.quarkus.narayana.jta.runtime.TransactionConfiguration; import io.quarkus.transaction.annotations.Rollback; import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; import io.smallrye.reactive.converters.ReactiveTypeConverter; import io.smallrye.reactive.converters.Registry; import mutiny.zero.flow.adapters.AdaptersToFlow; @@ -40,7 +41,7 @@ public abstract class TransactionalInterceptorBase implements Serializable { private static final long serialVersionUID = 1L; - private static final Logger log = Logger.getLogger(TransactionalInterceptorBase.class); + protected static final Logger log = Logger.getLogger(TransactionalInterceptorBase.class); private final Map methodTransactionTimeoutDefinedByPropertyCache = new ConcurrentHashMap<>(); @Inject @@ -433,4 +434,14 @@ protected void resetUserTransactionAvailability(boolean previousUserTransactionA private static void sneakyThrow(Throwable e) throws E { throw (E) e; } + + protected boolean disableInterceptorOnUniMethods(InvocationContext ic) throws Exception { + // Disable Interceptor on Reactive (uni) methods + // in The Reactive transaction module only REQUIRED is supported so far + if (ic.getMethod().getReturnType().equals(Uni.class)) { + log.debugf("method is annoted @Transactional but returns a Uni, JTA transactions will be disabled"); + return true; + } + return false; + } } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorMandatory.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorMandatory.java index 86cae40675547..6497d6f1bb20b 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorMandatory.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorMandatory.java @@ -32,6 +32,10 @@ public Object intercept(InvocationContext ic) throws Exception { @Override protected Object doIntercept(TransactionManager tm, Transaction tx, InvocationContext ic) throws Exception { + if (disableInterceptorOnUniMethods(ic)) { + return ic.proceed(); + } + if (tx == null) { throw new TransactionalException(jtaLogger.i18NLogger.get_tx_required(), new TransactionRequiredException()); } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java index 0fb2dd78db9e4..213311f348a69 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java @@ -10,6 +10,7 @@ import io.quarkus.runtime.BlockingOperationControl; import io.quarkus.runtime.BlockingOperationNotAllowedException; +import io.smallrye.mutiny.Uni; /** * @author paul.robinson@redhat.com 25/05/2013 @@ -26,6 +27,10 @@ public TransactionalInterceptorRequired() { @Override @AroundInvoke public Object intercept(InvocationContext ic) throws Exception { + if (disableInterceptorOnUniMethods(ic)) { + return ic.proceed(); + } + if (!BlockingOperationControl.isBlockingAllowed()) { throw new BlockingOperationNotAllowedException("Cannot start a JTA transaction from the IO thread."); } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java index 049fef8133bd1..d346e7be4aaf3 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java @@ -1,5 +1,6 @@ package io.quarkus.narayana.jta.runtime.interceptor; +import io.smallrye.mutiny.Uni; import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; @@ -26,6 +27,10 @@ public TransactionalInterceptorRequiresNew() { @Override @AroundInvoke public Object intercept(InvocationContext ic) throws Exception { + if (disableInterceptorOnUniMethods(ic)) { + return ic.proceed(); + } + if (!BlockingOperationControl.isBlockingAllowed()) { throw new BlockingOperationNotAllowedException("Cannot start a JTA transaction from the IO thread."); } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java index 5bfdadd421390..79d54982cabc7 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java @@ -4,7 +4,7 @@ import io.smallrye.mutiny.Uni; -abstract class AbstractUniInterceptor { +public abstract class AbstractUniInterceptor { @SuppressWarnings("unchecked") protected Uni proceedUni(InvocationContext context) { diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java index bb3cc826d4ca4..684ab5e0474d9 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java @@ -56,8 +56,9 @@ private static SessionFactory createSessionFactory(String persistenceunitname) { } private static Key createSessionKey(String persistenceUnitName) { + SessionFactory value = SESSION_FACTORY_MAP.getValue(persistenceUnitName); Implementor implementor = (Implementor) ClientProxy - .unwrap(SESSION_FACTORY_MAP.getValue(persistenceUnitName)); + .unwrap(value); return new BaseKey<>(Session.class, implementor.getUuid()); } @@ -67,6 +68,9 @@ private static Key createSessionKey(String persistenceUnitName) { // This key is used to keep track of the Set sessions created on demand private static final String SESSION_ON_DEMAND_OPENED_KEY = "hibernate.reactive.panache.sessionOnDemandOpened"; + // TODO Luca remove this once this module depends on reactive-transactional + private static final String TRANSACTIONAL_METHOD_KEY = "hibernate.reactive.methodTransactional"; + /** * Marks the current vertx duplicated context as "lazy" which indicates that a reactive session should be opened lazily if * needed. The opened session is eventually closed and the marking key is removed when the provided {@link Uni} completes. @@ -78,6 +82,13 @@ private static Key createSessionKey(String persistenceUnitName) { */ static Uni withSessionOnDemand(Supplier> work) { Context context = vertxContext(); + + if (context.getLocal(TRANSACTIONAL_METHOD_KEY) != null) { + return Uni.createFrom().failure( + new UnsupportedOperationException( + "Cannot call a method annotated with @WithSessionOnDemand from a method annotated with @Transactional")); + } + if (context.getLocal(SESSION_ON_DEMAND_KEY) != null) { // context already marked - no need to set the key and close the session return work.get(); @@ -233,7 +244,8 @@ public static Uni getSession(String persistenceUnitName) { */ public static Mutiny.Session getCurrentSession(String persistenceUnitName) { Context context = vertxContext(); - Mutiny.Session current = context.getLocal(SESSION_KEY_MAP.getValue(persistenceUnitName)); + Key value = SESSION_KEY_MAP.getValue(persistenceUnitName); + Mutiny.Session current = context.getLocal(value); if (current != null && current.isOpen()) { return current; } @@ -246,7 +258,7 @@ public static Mutiny.Session getCurrentSession(String persistenceUnitName) { * @throws IllegalStateException If no vertx context is found or is not a safe context as mandated by the * {@link VertxContextSafetyToggle} */ - private static Context vertxContext() { + public static Context vertxContext() { Context context = Vertx.currentContext(); if (context != null) { VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java index 6275bd3e2a42b..822bb7a60fc10 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java @@ -17,6 +17,7 @@ public Object intercept(InvocationContext context) throws Exception { // Bindings are validated at build time - method-level binding declared on a method that does not return Uni results in a build failure // However, a class-level binding implies that methods that do not return Uni are just a no-op if (isUniReturnType(context)) { + return SessionOperations.withSessionOnDemand(() -> proceedUni(context)); } return context.proceed(); diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java index 73dc54a1ea7bd..d6f05463319bf 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java @@ -1,22 +1,36 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; +import static io.quarkus.hibernate.reactive.runtime.HibernateReactiveRecorder.TRANSACTIONAL_METHOD_KEY; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; import io.quarkus.hibernate.reactive.panache.common.WithTransaction; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; @WithTransaction @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) public class WithTransactionInterceptor extends AbstractUniInterceptor { + public static final String WITH_TRANSACTION_METHOD_KEY = "hibernate.reactive.withTransaction"; + @AroundInvoke public Object intercept(InvocationContext context) throws Exception { // Bindings are validated at build time - method-level binding declared on a method that does not return Uni results in a build failure // However, a class-level binding implies that methods that do not return Uni are just a no-op if (isUniReturnType(context)) { + Context vertxContext = SessionOperations.vertxContext(); + if (vertxContext.getLocal(TRANSACTIONAL_METHOD_KEY) != null) { + return Uni.createFrom().failure( + new UnsupportedOperationException( + "Cannot call a method annotated with @WithTransaction from a method annotated with @Transactional")); + } + + vertxContext.putLocal(WITH_TRANSACTION_METHOD_KEY, true); String persistenceUnitName = getPersistenceUnitName(context); return SessionOperations.withTransaction(persistenceUnitName, () -> proceedUni(context)); } diff --git a/extensions/pom.xml b/extensions/pom.xml index 0bb24bc3a7af4..32b441bc9f77d 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -100,6 +100,7 @@ hibernate-orm hibernate-envers hibernate-reactive + hibernate-reactive-transactions hibernate-validator panache hibernate-search-backend-elasticsearch-common diff --git a/integration-tests/hibernate-reactive-transactions/pom.xml b/integration-tests/hibernate-reactive-transactions/pom.xml new file mode 100644 index 0000000000000..e8103cfcb04d5 --- /dev/null +++ b/integration-tests/hibernate-reactive-transactions/pom.xml @@ -0,0 +1,128 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + hibernate-reactive-transactions-integration-tests + Quarkus - Hibernate Reactive Transactions - Integration Tests + + + true + + + + + io.quarkus + quarkus-rest + + + io.quarkus + hibernate-reactive-transactions + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + + + io.quarkus + hibernate-reactive-transactions-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + false + true + + + + diff --git a/integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java b/integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java new file mode 100644 index 0000000000000..075c0db0d66e8 --- /dev/null +++ b/integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java @@ -0,0 +1,32 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package io.quarkus.hibernate.reactive.transactions.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/hibernate-reactive-transactions") +@ApplicationScoped +public class HibernateReactiveTransactionsResource { + // add some rest methods here + + @GET + public String hello() { + return "Hello hibernate-reactive-transactions"; + } +} diff --git a/integration-tests/hibernate-reactive-transactions/src/main/resources/application.properties b/integration-tests/hibernate-reactive-transactions/src/main/resources/application.properties new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceIT.java b/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceIT.java new file mode 100644 index 0000000000000..737d3b42e261b --- /dev/null +++ b/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.hibernate.reactive.transactions.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class HibernateReactiveTransactionsResourceIT extends HibernateReactiveTransactionsResourceTest { +} diff --git a/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java b/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java new file mode 100644 index 0000000000000..f8091b51a988f --- /dev/null +++ b/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java @@ -0,0 +1,21 @@ +package io.quarkus.hibernate.reactive.transactions.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class HibernateReactiveTransactionsResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/hibernate-reactive-transactions") + .then() + .statusCode(200) + .body(is("Hello hibernate-reactive-transactions")); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 6e1920f2f8b0a..0ba12d288ee0d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -217,6 +217,7 @@ hibernate-reactive-panache hibernate-reactive-panache-kotlin hibernate-reactive-orm-compatibility + hibernate-reactive-transactions hibernate-search-orm-elasticsearch hibernate-search-orm-elasticsearch-outbox-polling hibernate-search-orm-opensearch diff --git a/pom.xml b/pom.xml index 24870d44bfa2b..9ef61b5c2b392 100644 --- a/pom.xml +++ b/pom.xml @@ -71,12 +71,12 @@ 0.8.14 7.4.0 5.5.6 - 7.1.6.Final + 7.2.0.CR1 3.2.0 4.13.2 1.17.6 1.0.1 - 3.1.8.Final + 3.2.0-SNAPSHOT 9.1.0.Final 8.1.2.Final 7.1.6.Final From 6ebd205085e5ca384932affcc513d6c21908f510 Mon Sep 17 00:00:00 2001 From: Luca Molteni Date: Thu, 13 Nov 2025 10:58:06 +0100 Subject: [PATCH 2/4] Module name changes: Renamed module name from hibernate-reactive-transactions => quarkus-reactive-transactions Removed panache dependency from transaction (was test) Adds quarkus-reactive-transactions dependency to hibernate-reactive-panache-common/runtime and quarkus-reactive-transactions-deployment dependency to hibernate-reactive-panache/deployment to support the relocated tests. Move @Transactional mixing tests to hibernate-reactive-panache --- bom/application/pom.xml | 4 ++-- .../deployment/pom.xml | 11 +++-------- .../HibernateReactiveTransactionsProcessor.java | 2 +- extensions/hibernate-reactive-transactions/pom.xml | 2 +- .../hibernate-reactive-transactions/runtime/pom.xml | 4 ++-- .../main/resources/META-INF/quarkus-extension.yaml | 2 +- .../interceptor/TransactionalInterceptorRequired.java | 1 - .../TransactionalInterceptorRequiresNew.java | 1 - .../hibernate-reactive-panache-common/runtime/pom.xml | 4 ++++ .../hibernate-reactive-panache/deployment/pom.xml | 4 ++++ ...SessionOnDemandAndTransactionalSameMethodTest.java | 2 +- .../test/transaction}/MixWithSessionOnDemandTest.java | 2 +- .../test/transaction}/MixWithTransactionTest.java | 2 +- ...MixWithTransactionTransactionalSameMethodTest.java | 2 +- .../hibernate-reactive-transactions/pom.xml | 6 +++--- .../it/HibernateReactiveTransactionsResource.java | 4 ++-- .../it/HibernateReactiveTransactionsResourceTest.java | 4 ++-- 17 files changed, 29 insertions(+), 28 deletions(-) rename extensions/{hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing => panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction}/MixWithSessionOnDemandAndTransactionalSameMethodTest.java (95%) rename extensions/{hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing => panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction}/MixWithSessionOnDemandTest.java (97%) rename extensions/{hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing => panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction}/MixWithTransactionTest.java (97%) rename extensions/{hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing => panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction}/MixWithTransactionTransactionalSameMethodTest.java (95%) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 2bbdf2a5a930a..6d06ff3f1c6bf 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1386,12 +1386,12 @@ io.quarkus - hibernate-reactive-transactions + quarkus-reactive-transactions ${project.version} io.quarkus - hibernate-reactive-transactions-deployment + quarkus-reactive-transactions-deployment ${project.version} diff --git a/extensions/hibernate-reactive-transactions/deployment/pom.xml b/extensions/hibernate-reactive-transactions/deployment/pom.xml index 2c72265da81d6..8d38dae60331a 100644 --- a/extensions/hibernate-reactive-transactions/deployment/pom.xml +++ b/extensions/hibernate-reactive-transactions/deployment/pom.xml @@ -5,10 +5,10 @@ io.quarkus - hibernate-reactive-transactions-parent + quarkus-reactive-transactions-parent 999-SNAPSHOT - hibernate-reactive-transactions-deployment + quarkus-reactive-transactions-deployment Quarkus - Hibernate Reactive Transactions - Deployment @@ -18,7 +18,7 @@ io.quarkus - hibernate-reactive-transactions + quarkus-reactive-transactions io.quarkus @@ -57,11 +57,6 @@ quarkus-reactive-pg-client-deployment test - - io.quarkus - quarkus-hibernate-reactive-panache-common-deployment - test - io.quarkus quarkus-narayana-jta diff --git a/extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java b/extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java index 7fc7f13c74303..9f3b4549fa36a 100644 --- a/extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java +++ b/extensions/hibernate-reactive-transactions/deployment/src/main/java/io/quarkus/hibernate/reactive/transactions/deployment/HibernateReactiveTransactionsProcessor.java @@ -17,7 +17,7 @@ class HibernateReactiveTransactionsProcessor { - private static final String FEATURE = "hibernate-reactive-transactions"; + private static final String FEATURE = "quarkus-reactive-transactions"; private static final DotName TRANSACTIONAL = DotName.createSimple(Transactional.class.getName()); diff --git a/extensions/hibernate-reactive-transactions/pom.xml b/extensions/hibernate-reactive-transactions/pom.xml index 868c199d7f51a..a7dd38e80d660 100644 --- a/extensions/hibernate-reactive-transactions/pom.xml +++ b/extensions/hibernate-reactive-transactions/pom.xml @@ -7,7 +7,7 @@ quarkus-extensions-parent 999-SNAPSHOT - hibernate-reactive-transactions-parent + quarkus-reactive-transactions-parent pom Quarkus - Hibernate Reactive Transactions - Parent diff --git a/extensions/hibernate-reactive-transactions/runtime/pom.xml b/extensions/hibernate-reactive-transactions/runtime/pom.xml index 1ad6a11aa6f30..32c1d6b3e7afe 100644 --- a/extensions/hibernate-reactive-transactions/runtime/pom.xml +++ b/extensions/hibernate-reactive-transactions/runtime/pom.xml @@ -4,10 +4,10 @@ io.quarkus - hibernate-reactive-transactions-parent + quarkus-reactive-transactions-parent 999-SNAPSHOT - hibernate-reactive-transactions + quarkus-reactive-transactions Quarkus - Hibernate Reactive Transactions - Runtime Quarkus - Hibernate Reactive Transaction manager diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml index c9fa622b4b3de..d5cf048e1be5e 100644 --- a/extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -2,7 +2,7 @@ name: Quarkus - Hibernate Reactive Transactions #description: Quarkus - Hibernate Reactive Transaction manager metadata: # keywords: -# - hibernate-reactive-transactions +# - quarkus-reactive-transactions # guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension # categories: # - "miscellaneous" diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java index 213311f348a69..2cb397133aa4d 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequired.java @@ -10,7 +10,6 @@ import io.quarkus.runtime.BlockingOperationControl; import io.quarkus.runtime.BlockingOperationNotAllowedException; -import io.smallrye.mutiny.Uni; /** * @author paul.robinson@redhat.com 25/05/2013 diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java index d346e7be4aaf3..ffc027caeeedd 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorRequiresNew.java @@ -1,6 +1,5 @@ package io.quarkus.narayana.jta.runtime.interceptor; -import io.smallrye.mutiny.Uni; import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/pom.xml b/extensions/panache/hibernate-reactive-panache-common/runtime/pom.xml index b600f48dc5044..0c2cff32d9999 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/pom.xml +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/pom.xml @@ -32,6 +32,10 @@ io.quarkus quarkus-panache-hibernate-common + + io.quarkus + quarkus-reactive-transactions + org.junit.jupiter junit-jupiter diff --git a/extensions/panache/hibernate-reactive-panache/deployment/pom.xml b/extensions/panache/hibernate-reactive-panache/deployment/pom.xml index ea051e1cfbb21..afb3ab220e4da 100644 --- a/extensions/panache/hibernate-reactive-panache/deployment/pom.xml +++ b/extensions/panache/hibernate-reactive-panache/deployment/pom.xml @@ -28,6 +28,10 @@ io.quarkus quarkus-panache-hibernate-common-deployment + + io.quarkus + quarkus-reactive-transactions-deployment + diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandAndTransactionalSameMethodTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandAndTransactionalSameMethodTest.java similarity index 95% rename from extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandAndTransactionalSameMethodTest.java rename to extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandAndTransactionalSameMethodTest.java index 1dc6282699f35..9e9421a64fd70 100644 --- a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandAndTransactionalSameMethodTest.java +++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandAndTransactionalSameMethodTest.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.reactive.transactions.test.mixing; +package io.quarkus.hibernate.reactive.panache.test.transaction; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandTest.java similarity index 97% rename from extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandTest.java rename to extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandTest.java index 9895b78669c52..437c9e9a9b0f8 100644 --- a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithSessionOnDemandTest.java +++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandTest.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.reactive.transactions.test.mixing; +package io.quarkus.hibernate.reactive.panache.test.transaction; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTest.java similarity index 97% rename from extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTest.java rename to extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTest.java index 061b13d866c7e..cf1fbcb151a65 100644 --- a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTest.java +++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTest.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.reactive.transactions.test.mixing; +package io.quarkus.hibernate.reactive.panache.test.transaction; import static org.assertj.core.api.Assertions.assertThat; diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTransactionalSameMethodTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTransactionalSameMethodTest.java similarity index 95% rename from extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTransactionalSameMethodTest.java rename to extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTransactionalSameMethodTest.java index 85d94be655a13..b8fe37f68318b 100644 --- a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/mixing/MixWithTransactionTransactionalSameMethodTest.java +++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTransactionalSameMethodTest.java @@ -1,4 +1,4 @@ -package io.quarkus.hibernate.reactive.transactions.test.mixing; +package io.quarkus.hibernate.reactive.panache.test.transaction; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; diff --git a/integration-tests/hibernate-reactive-transactions/pom.xml b/integration-tests/hibernate-reactive-transactions/pom.xml index e8103cfcb04d5..8e5dda92aebb6 100644 --- a/integration-tests/hibernate-reactive-transactions/pom.xml +++ b/integration-tests/hibernate-reactive-transactions/pom.xml @@ -7,7 +7,7 @@ quarkus-integration-tests-parent 999-SNAPSHOT - hibernate-reactive-transactions-integration-tests + quarkus-reactive-transactions-integration-tests Quarkus - Hibernate Reactive Transactions - Integration Tests @@ -21,7 +21,7 @@ io.quarkus - hibernate-reactive-transactions + quarkus-reactive-transactions io.quarkus @@ -40,7 +40,7 @@ io.quarkus - hibernate-reactive-transactions-deployment + quarkus-reactive-transactions-deployment ${project.version} pom test diff --git a/integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java b/integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java index 075c0db0d66e8..fafc711089490 100644 --- a/integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java +++ b/integration-tests/hibernate-reactive-transactions/src/main/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResource.java @@ -20,13 +20,13 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; -@Path("/hibernate-reactive-transactions") +@Path("/quarkus-reactive-transactions") @ApplicationScoped public class HibernateReactiveTransactionsResource { // add some rest methods here @GET public String hello() { - return "Hello hibernate-reactive-transactions"; + return "Hello quarkus-reactive-transactions"; } } diff --git a/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java b/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java index f8091b51a988f..5163aebc8612c 100644 --- a/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java +++ b/integration-tests/hibernate-reactive-transactions/src/test/java/io/quarkus/hibernate/reactive/transactions/it/HibernateReactiveTransactionsResourceTest.java @@ -13,9 +13,9 @@ public class HibernateReactiveTransactionsResourceTest { @Test public void testHelloEndpoint() { given() - .when().get("/hibernate-reactive-transactions") + .when().get("/quarkus-reactive-transactions") .then() .statusCode(200) - .body(is("Hello hibernate-reactive-transactions")); + .body(is("Hello quarkus-reactive-transactions")); } } From 214c53b62314454027c5420aca73c03157cef9cb Mon Sep 17 00:00:00 2001 From: Luca Molteni Date: Thu, 13 Nov 2025 15:51:26 +0100 Subject: [PATCH 3/4] Common methods / constant in transaction / Panache import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; Moved the WITH_TRANSACTION_METHOD_KEY constant inside Transaction module Moved the SESSION_ON_DEMAND_KEY constant inside Transaction module --- .../runtime/TransactionalInterceptorBase.java | 13 ++++++----- .../runtime/HibernateReactiveRecorder.java | 3 --- .../runtime/AbstractUniInterceptor.java | 22 ------------------- .../ReactiveTransactionalInterceptor.java | 4 +++- .../common/runtime/SessionOperations.java | 4 +--- .../runtime/WithSessionInterceptor.java | 5 ++++- .../WithSessionOnDemandInterceptor.java | 5 ++++- .../runtime/WithTransactionInterceptor.java | 7 +++--- 8 files changed, 23 insertions(+), 40 deletions(-) delete mode 100644 extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java index 1fdc10d0c4b15..97bef239493b1 100644 --- a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java @@ -1,6 +1,5 @@ package io.quarkus.hibernate.reactive.transactions.runtime; -import static io.quarkus.hibernate.reactive.runtime.HibernateReactiveRecorder.WITH_TRANSACTION_METHOD_KEY; import static io.quarkus.hibernate.reactive.runtime.customized.TransactionalContextPool.CURRENT_TRANSACTION_KEY; import java.util.Optional; @@ -71,9 +70,8 @@ Uni rollback() { .toCompletionStage()); } - // TODO copied from Panache -- refactor and put in a common module? @SuppressWarnings("unchecked") - protected Uni proceedUni(InvocationContext context) { + public static Uni proceedUni(InvocationContext context) { try { return ((Uni) context.proceed()); } catch (Exception e) { @@ -82,7 +80,7 @@ protected Uni proceedUni(InvocationContext context) { } // TODO copied from Panache -- refactor and put in a common module? - protected boolean isUniReturnType(InvocationContext context) { + public static boolean isUniReturnType(InvocationContext context) { return context.getMethod().getReturnType().equals(Uni.class); } @@ -92,8 +90,11 @@ protected boolean isUniReturnType(InvocationContext context) { // TODO Luca find a way to remove the duplication between this field and TransactionalInterceptor field private static final String TRANSACTIONAL_METHOD_KEY = "hibernate.reactive.methodTransactional"; - // This key is copied from panache and it's the marker key the WithSessionOnDemand intereceptor uses - private static final String SESSION_ON_DEMAND_KEY = "hibernate.reactive.panache.sessionOnDemand"; + // This key is used by Panache internally it's the marker key the WithSessionOnDemand intereceptor uses + public static final String SESSION_ON_DEMAND_KEY = "hibernate.reactive.panache.sessionOnDemand"; + + // This key is used by Panache internally it's the marker key the WithTransaction intereceptor uses + public static final String WITH_TRANSACTION_METHOD_KEY = "hibernate.reactive.withTransaction"; static Uni withTransactionalSessionOnDemand(Supplier> work) { Context context = vertxContext(); diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java index 4bafdda79e16f..d5c5b52d62998 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java @@ -122,9 +122,6 @@ public Mutiny.Session delegate() { // TODO Luca find a way to remove the duplication between this field and TransactionalInterceptor TRANSACTIONAL_METHOD_KEY field public static final String TRANSACTIONAL_METHOD_KEY = "hibernate.reactive.methodTransactional"; - // TODO Luca find a way to remove duplication - public static final String WITH_TRANSACTION_METHOD_KEY = "hibernate.reactive.withTransaction"; - public static Mutiny.Session getSession(String persistenceUnitName) { Context context = Vertx.currentContext(); diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java deleted file mode 100644 index 79d54982cabc7..0000000000000 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkus.hibernate.reactive.panache.common.runtime; - -import jakarta.interceptor.InvocationContext; - -import io.smallrye.mutiny.Uni; - -public abstract class AbstractUniInterceptor { - - @SuppressWarnings("unchecked") - protected Uni proceedUni(InvocationContext context) { - try { - return ((Uni) context.proceed()); - } catch (Exception e) { - return Uni.createFrom().failure(e); - } - } - - protected boolean isUniReturnType(InvocationContext context) { - return context.getMethod().getReturnType().equals(Uni.class); - } - -} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java index fe3e3f797ef3a..5405e290fd1ee 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java @@ -5,10 +5,12 @@ import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; + @ReactiveTransactional @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) -public class ReactiveTransactionalInterceptor extends AbstractUniInterceptor { +public class ReactiveTransactionalInterceptor { @AroundInvoke public Object intercept(InvocationContext context) throws Exception { diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java index 684ab5e0474d9..83a41938eb388 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java @@ -1,6 +1,7 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; import static io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.SESSION_ON_DEMAND_KEY; import java.util.ArrayList; import java.util.HashSet; @@ -62,9 +63,6 @@ private static Key createSessionKey(String persistenceUnitName) { return new BaseKey<>(Session.class, implementor.getUuid()); } - // This key is used to indicate that reactive sessions should be opened lazily/on-demand (when needed) in the current vertx context - private static final String SESSION_ON_DEMAND_KEY = "hibernate.reactive.panache.sessionOnDemand"; - // This key is used to keep track of the Set sessions created on demand private static final String SESSION_ON_DEMAND_OPENED_KEY = "hibernate.reactive.panache.sessionOnDemandOpened"; diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java index cd8de943a973a..16208ae240fa0 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java @@ -7,10 +7,13 @@ import io.quarkus.hibernate.reactive.panache.common.WithSession; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; + @WithSession @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) -public class WithSessionInterceptor extends AbstractUniInterceptor { +public class WithSessionInterceptor { @AroundInvoke public Object intercept(InvocationContext context) throws Exception { diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java index 822bb7a60fc10..1ea47aff62c83 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java @@ -7,10 +7,13 @@ import io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; + @WithSessionOnDemand @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) -public class WithSessionOnDemandInterceptor extends AbstractUniInterceptor { +public class WithSessionOnDemandInterceptor { @AroundInvoke public Object intercept(InvocationContext context) throws Exception { diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java index d6f05463319bf..facf27d1f062f 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java @@ -1,6 +1,9 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; import static io.quarkus.hibernate.reactive.runtime.HibernateReactiveRecorder.TRANSACTIONAL_METHOD_KEY; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.WITH_TRANSACTION_METHOD_KEY; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; @@ -14,9 +17,7 @@ @WithTransaction @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) -public class WithTransactionInterceptor extends AbstractUniInterceptor { - - public static final String WITH_TRANSACTION_METHOD_KEY = "hibernate.reactive.withTransaction"; +public class WithTransactionInterceptor { @AroundInvoke public Object intercept(InvocationContext context) throws Exception { From d53862cb86d59b5fe99ae9c238962147ad19b03f Mon Sep 17 00:00:00 2001 From: Luca Molteni Date: Thu, 13 Nov 2025 17:31:45 +0100 Subject: [PATCH 4/4] Support injection for Stateless session as well --- ...nateReactiveStatelessTransactionsTest.java | 120 ++++++++++++++++++ .../runtime/TransactionalInterceptorBase.java | 5 +- .../runtime/HibernateReactiveRecorder.java | 31 +++-- .../reactive/runtime/OpenedSessionsState.java | 81 +++++++----- .../OpenedSessionsStateStatefulImpl.java | 21 +++ .../OpenedSessionsStateStatelessImpl.java | 20 +++ .../customized/TransactionalContextPool.java | 11 +- .../ReactiveTransactionalInterceptor.java | 4 +- .../runtime/WithSessionInterceptor.java | 6 +- .../WithSessionOnDemandInterceptor.java | 6 +- .../runtime/WithTransactionInterceptor.java | 2 +- 11 files changed, 253 insertions(+), 54 deletions(-) create mode 100644 extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveStatelessTransactionsTest.java create mode 100644 extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatefulImpl.java create mode 100644 extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatelessImpl.java diff --git a/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveStatelessTransactionsTest.java b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveStatelessTransactionsTest.java new file mode 100644 index 0000000000000..e7d8d78d4f0e1 --- /dev/null +++ b/extensions/hibernate-reactive-transactions/deployment/src/test/java/io/quarkus/hibernate/reactive/transactions/test/HibernateReactiveStatelessTransactionsTest.java @@ -0,0 +1,120 @@ +package io.quarkus.hibernate.reactive.transactions.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.hibernate.reactive.mutiny.Mutiny; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorRequired; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; +import io.smallrye.mutiny.Uni; + +public class HibernateReactiveStatelessTransactionsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar + .addClasses(Hero.class, TransactionalInterceptorRequired.class) + .addAsResource("initialTransactionData.sql", "import.sql")) + .withConfigurationResource("application.properties"); + + @Inject + Mutiny.SessionFactory sessionFactory; + + /** + * This test shows how to use hibernate reactive .withStatelessTransaction to set transactional boundaries + * Below there's testReactiveAnnotationTransaction which is the same test but with @Transactional + * + * @param asserter + */ + @Test + @RunOnVertxContext + public void testReactiveManualTransaction(UniAsserter asserter) { + // initialTransactionData.sql + Long heroId = 60L; + + // First update, make sure it's committed + asserter.assertThat( + () -> sessionFactory.withStatelessTransaction(session -> updateHero(session, heroId, "updatedNameCommitted")) + // 2nd endpoint call + .chain(() -> sessionFactory.withStatelessTransaction(session -> session.get(Hero.class, heroId))), + h -> assertThat(h.name).isEqualTo("updatedNameCommitted")); + + // Second update, make sure there's a rollback + asserter.assertThat( + () -> sessionFactory.withStatelessTransaction(session -> { + return updateHero(session, heroId, "this name won't appear") + .onItem().invoke(h -> { + throw new RuntimeException("Failing update"); + }); + }).onFailure().recoverWithNull() + .chain(() -> sessionFactory.withStatelessTransaction(session -> session.get(Hero.class, heroId))), + h -> { + assertThat(h.name).isEqualTo("updatedNameCommitted"); + }); + } + + @Inject + Mutiny.StatelessSession session; + + /* + * This is the same test as #testReactiveManualTransaction but instead of manually calling + * sessionFactory.withStatelessTransaction + * We use the annotation @Transactional + */ + @Test + @RunOnVertxContext + public void testReactiveAnnotationTransaction(UniAsserter asserter) { + // initialTransactionData.sql + Long heroId = 50L; + + // First update, make sure it's committed + asserter.assertThat( + () -> updateWithCommit(heroId, "updatedNameCommitted") + .chain(() -> findHero(heroId)), + h -> { + assertThat(h.name).isEqualTo("updatedNameCommitted"); + }); + + // Second update, make sure there's a rollback + asserter.assertThat( + () -> transactionalUpdateWithRollback(heroId, "this name won't appear") + .onFailure().recoverWithNull() + .chain(() -> findHero(heroId)), + h -> { + assertThat(h.name).isEqualTo("updatedNameCommitted"); + }); + } + + @Transactional + public Uni findHero(Long previousHeroId) { + return session.get(Hero.class, previousHeroId); + } + + @Transactional + public Uni updateWithCommit(Long previousHeroId, String newName) { + return updateHero(session, previousHeroId, newName); + } + + @Transactional + public Uni transactionalUpdateWithRollback(Long previousHeroId, String newName) { + return updateHero(session, previousHeroId, newName) + .onItem().invoke(h -> { + throw new RuntimeException("Failing update"); + }); + } + + public Uni updateHero(Mutiny.StatelessSession session, Long id, String newName) { + return session.get(Hero.class, id) + .map(h -> { + h.setName(newName); + return h; + }).onItem().call(h -> session.update(h)); + } +} diff --git a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java index 97bef239493b1..e540c0ddb3ffc 100644 --- a/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java +++ b/extensions/hibernate-reactive-transactions/runtime/src/main/java/io/quarkus/hibernate/reactive/transactions/runtime/TransactionalInterceptorBase.java @@ -123,7 +123,10 @@ static Uni withTransactionalSessionOnDemand(Supplier> work) { // perform the work and eventually close the session and remove the key return work.get().eventually(() -> { context.removeLocal(TRANSACTIONAL_METHOD_KEY); - return HibernateReactiveRecorder.OPENED_SESSIONS_STATE.closeAllOpenedSessions(context); + return Uni.combine().all().unis( + HibernateReactiveRecorder.OPENED_SESSIONS_STATE.closeAllOpenedSessions(context), + HibernateReactiveRecorder.OPENED_SESSIONS_STATE_STATLESS.closeAllOpenedSessions(context)) + .discardItems(); }); } } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java index d5c5b52d62998..d18af03dc956a 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/HibernateReactiveRecorder.java @@ -33,7 +33,8 @@ public HibernateReactiveRecorder(final RuntimeValue r this.runtimeConfig = runtimeConfig; } - public static final OpenedSessionsState OPENED_SESSIONS_STATE = new OpenedSessionsState(); + public static final OpenedSessionsState OPENED_SESSIONS_STATE = new OpenedSessionsStateStatefulImpl(); + public static final OpenedSessionsState OPENED_SESSIONS_STATE_STATLESS = new OpenedSessionsStateStatelessImpl(); /** * The feature needs to be initialized, even if it's not enabled. @@ -125,7 +126,8 @@ public Mutiny.Session delegate() { public static Mutiny.Session getSession(String persistenceUnitName) { Context context = Vertx.currentContext(); - Optional openedSession = OPENED_SESSIONS_STATE.getOpenedSession(context, + Optional> openedSession = OPENED_SESSIONS_STATE.getOpenedSession( + context, persistenceUnitName); // reuse the existing reactive session if (openedSession.isPresent()) { @@ -149,16 +151,29 @@ public Mutiny.StatelessSession apply(SyntheticCreationalContext returns Mutiny.Session - - throw new UnsupportedOperationException(); - + return getStatelessSession(persistenceUnitName); } }; } }; } + public static Mutiny.StatelessSession getStatelessSession(String persistenceUnitName) { + Context context = Vertx.currentContext(); + + Optional> openedSession = OPENED_SESSIONS_STATE_STATLESS + .getOpenedSession(context, persistenceUnitName); + // reuse the existing reactive session + if (openedSession.isPresent()) { + return openedSession.get().session(); + } else if (context.getLocal(TRANSACTIONAL_METHOD_KEY) == null) { + throw new IllegalStateException("No current Mutiny.Session found" + + "\n\t- no reactive session was found in the Vert.x context and the context was not marked to open a new session lazily" + + "\n\t- a session is opened automatically for JAX-RS resource methods annotated with an HTTP method (@GET, @POST, etc.); inherited annotations are not taken into account" + + "\n\t- you may need to annotate the business method with @Transactional"); + } else { + return OPENED_SESSIONS_STATE_STATLESS.createNewSession(persistenceUnitName, context); + } + } + } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java index 5ce0ee32cb46c..b6639e380b9e3 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsState.java @@ -12,7 +12,6 @@ import org.hibernate.reactive.context.Context.Key; import org.hibernate.reactive.context.impl.BaseKey; import org.hibernate.reactive.mutiny.Mutiny; -import org.hibernate.reactive.mutiny.impl.MutinySessionImpl; import io.quarkus.arc.Arc; import io.quarkus.arc.ClientProxy; @@ -21,30 +20,39 @@ import io.smallrye.mutiny.Uni; import io.vertx.core.Context; -public class OpenedSessionsState { +public abstract class OpenedSessionsState { // This key is used to keep track of the Set sessions created on demand - private static final String SESSIONS_ON_DEMAND_OPENED_KEY = "hibernate.reactive.panache.sessionOnDemandOpened"; + private final String sessionOnDemandKey; - private final ComputingCache> sessionKeys = new ComputingCache<>( - k -> createSessionFactoryAndStoreKey(k)); + protected abstract T newSessionMethod(Mutiny.SessionFactory sessionFactory); + + protected abstract Class getSessionType(); + + protected abstract boolean isSessionOpen(T session); + + private final ComputingCache> sessionKeys = new ComputingCache<>( + k -> createKeyForSessionType(k)); private final ComputingCache sessionFactories = new ComputingCache<>( k -> createSessionFactory(k)); - public record SessionWithKey(org.hibernate.reactive.context.Context.Key key, Mutiny.Session session) { + protected OpenedSessionsState() { + sessionOnDemandKey = "hibernate.reactive.panache.sessionOnDemandOpened" + getSessionType().getName(); + } + public record SessionWithKey(org.hibernate.reactive.context.Context.Key key, T session) { } - public Optional getOpenedSession(Context context, String persistenceUnitName) { - Key sessionKey = sessionKeys.getValue(persistenceUnitName); + public Optional> getOpenedSession(Context context, String persistenceUnitName) { + Key sessionKey = sessionKeys.getValue(persistenceUnitName); return getOpenedSession(context, sessionKey); } - private static Optional getOpenedSession(Context context, Key sessionKey) { - Mutiny.Session current = context.getLocal(sessionKey); + private Optional> getOpenedSession(Context context, Key sessionKey) { + T current = context.getLocal(sessionKey); return Optional.ofNullable(current) - .filter(s -> s.isOpen()) - .map(s -> new SessionWithKey(sessionKey, s)); + .filter(s -> isSessionOpen(s)) + .map(s -> new SessionWithKey<>(sessionKey, s)); } public Uni closeAllOpenedSessions(Context context) { @@ -53,54 +61,57 @@ public Uni closeAllOpenedSessions(Context context) { return Uni.createFrom().voidItem(); } List> closedSessionsUnis = new ArrayList<>(); - for (String s : onDemandSessionCreated) { - Optional openedSession = getOpenedSession(context, s); - closedSessionsUnis.add(closeAndRemoveSession(context, openedSession)); + for (String sessionName : onDemandSessionCreated) { + + Uni closeSessionUni = getOpenedSession(context, sessionName) + .map(session -> closeAndRemoveSession(context, session)) + .orElse(Uni.createFrom().voidItem()); + + closedSessionsUnis.add(closeSessionUni); } - context.removeLocal(SESSIONS_ON_DEMAND_OPENED_KEY); + context.removeLocal(sessionOnDemandKey); return Uni.combine().all().unis(closedSessionsUnis).discardItems(); } - public Mutiny.Session createNewSession(String persistenceUnitName, Context context) { + public T createNewSession(String persistenceUnitName, Context context) { + Mutiny.SessionFactory sessionFactory = sessionFactories.getValue(persistenceUnitName); + T session = newSessionMethod(sessionFactory); + Key sessionKey = sessionKeys.getValue(persistenceUnitName); - Set openedSession = openedSessionContextSet(context); + storeSession(context, persistenceUnitName, sessionKey, session); + return session; + } + private void storeSession(Context context, String persistenceUnitName, Key sessionKey, T session) { + Set openedSession = openedSessionContextSet(context); // open a new reactive session and store it in the vertx duplicated context // the context was marked as "lazy" which means that the session will be eventually closed openedSession.add(persistenceUnitName); - - Mutiny.SessionFactory sessionFactory = sessionFactories.getValue(persistenceUnitName); - MutinySessionImpl session = (MutinySessionImpl) sessionFactory.createSession(); - - Key sessionKey = sessionKeys.getValue(persistenceUnitName); context.putLocal(sessionKey, session); - - return session; } private Set openedSessionContextSet(Context context) { // This will keep track of all on-demand opened sessions - Set onDemandSessionsCreated = context.getLocal(SESSIONS_ON_DEMAND_OPENED_KEY); + Set onDemandSessionsCreated = context.getLocal(sessionOnDemandKey); if (onDemandSessionsCreated == null) { onDemandSessionsCreated = new HashSet<>(); - context.putLocal(SESSIONS_ON_DEMAND_OPENED_KEY, onDemandSessionsCreated); + context.putLocal(sessionOnDemandKey, onDemandSessionsCreated); } return onDemandSessionsCreated; } - private Uni closeAndRemoveSession(Context context, Optional openSession) { - return openSession.map((SessionWithKey s) -> s.session.close() - .eventually(() -> context.removeLocal(s.key))) - .orElse(Uni.createFrom().voidItem()); + private Uni closeAndRemoveSession(Context context, SessionWithKey openSession) { + return openSession.session.close() + .eventually(() -> context.removeLocal(openSession.key)); } - private Key createSessionFactoryAndStoreKey(String persistenceUnitName) { + private Key createKeyForSessionType(String persistenceUnitName) { Mutiny.SessionFactory value = createSessionFactory(persistenceUnitName); Implementor implementor = (Implementor) ClientProxy.unwrap(value); - return new BaseKey<>(Mutiny.Session.class, implementor.getUuid()); + return new BaseKey<>(getSessionType(), implementor.getUuid()); } - private Mutiny.SessionFactory createSessionFactory(String persistenceunitname) { + private static Mutiny.SessionFactory createSessionFactory(String persistenceunitname) { Mutiny.SessionFactory sessionFactory; // Note that Mutiny.SessionFactory is @ApplicationScoped bean - it's safe to use the cached client proxy @@ -116,4 +127,4 @@ private Mutiny.SessionFactory createSessionFactory(String persistenceunitname) { } return sessionFactory; } -} +} \ No newline at end of file diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatefulImpl.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatefulImpl.java new file mode 100644 index 0000000000000..ceff9ff6e6abc --- /dev/null +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatefulImpl.java @@ -0,0 +1,21 @@ +package io.quarkus.hibernate.reactive.runtime; + +import org.hibernate.reactive.mutiny.Mutiny; + +public class OpenedSessionsStateStatefulImpl extends OpenedSessionsState { + + @Override + protected Mutiny.Session newSessionMethod(Mutiny.SessionFactory sessionFactory) { + return sessionFactory.createSession(); + } + + @Override + protected Class getSessionType() { + return Mutiny.Session.class; + } + + @Override + protected boolean isSessionOpen(Mutiny.Session session) { + return session.isOpen(); + } +} diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatelessImpl.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatelessImpl.java new file mode 100644 index 0000000000000..5fb9c3a4936f7 --- /dev/null +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/OpenedSessionsStateStatelessImpl.java @@ -0,0 +1,20 @@ +package io.quarkus.hibernate.reactive.runtime; + +import org.hibernate.reactive.mutiny.Mutiny; + +public class OpenedSessionsStateStatelessImpl extends OpenedSessionsState { + @Override + protected Mutiny.StatelessSession newSessionMethod(Mutiny.SessionFactory sessionFactory) { + return sessionFactory.createStatelessSession(); + } + + @Override + protected Class getSessionType() { + return Mutiny.StatelessSession.class; + } + + @Override + protected boolean isSessionOpen(Mutiny.StatelessSession session) { + return session.isOpen(); + } +} diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java index 73461e0ef16c3..ae28670d7679a 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/customized/TransactionalContextPool.java @@ -4,6 +4,8 @@ import java.util.function.Function; +import org.jboss.logging.Logger; + import io.vertx.core.AsyncResult; import io.vertx.core.Context; import io.vertx.core.Future; @@ -17,6 +19,7 @@ import io.vertx.sqlclient.Row; import io.vertx.sqlclient.RowSet; import io.vertx.sqlclient.SqlConnection; +import io.vertx.sqlclient.Transaction; /** * A pool that handles transaction based on Vert.x context set by the @Transactional interceptor. @@ -51,15 +54,21 @@ public void getConnection(Handler> handler) { } } + private static final Logger LOG = Logger.getLogger(TransactionalContextPool.class); + @Override public Future getConnection() { if (!shouldOpenTransaction()) { return delegate.getConnection(); } else { + LOG.tracef("Getting a new connection"); return delegate.getConnection() .compose(connection -> { + LOG.tracef("New connection, about to start transaction: %s", connection); return connection.begin().map(t -> { - Vertx.currentContext().putLocal(CURRENT_TRANSACTION_KEY, connection.transaction()); + Transaction transaction = connection.transaction(); + LOG.tracef("Transaction started: %s", transaction); + Vertx.currentContext().putLocal(CURRENT_TRANSACTION_KEY, transaction); return new TransactionalContextConnection(connection); }); }); diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java index 5405e290fd1ee..67f91fbf33320 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java @@ -1,12 +1,12 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; import jakarta.interceptor.InvocationContext; -import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; - @ReactiveTransactional @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java index 16208ae240fa0..bdfeb2de16c77 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java @@ -1,5 +1,8 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; @@ -7,9 +10,6 @@ import io.quarkus.hibernate.reactive.panache.common.WithSession; -import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; -import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; - @WithSession @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java index 1ea47aff62c83..89c32856daf96 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java @@ -1,5 +1,8 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; +import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; + import jakarta.annotation.Priority; import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; @@ -7,9 +10,6 @@ import io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand; -import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.isUniReturnType; -import static io.quarkus.hibernate.reactive.transactions.runtime.TransactionalInterceptorBase.proceedUni; - @WithSessionOnDemand @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java index facf27d1f062f..eac3381640d0f 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java @@ -17,7 +17,7 @@ @WithTransaction @Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) -public class WithTransactionInterceptor { +public class WithTransactionInterceptor { @AroundInvoke public Object intercept(InvocationContext context) throws Exception {