diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 528a842d688bf..6d06ff3f1c6bf 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -1384,6 +1384,16 @@
quarkus-hibernate-reactive-deployment
${project.version}
+
+ io.quarkus
+ quarkus-reactive-transactions
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-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..8d38dae60331a
--- /dev/null
+++ b/extensions/hibernate-reactive-transactions/deployment/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+
+ io.quarkus
+ quarkus-reactive-transactions-parent
+ 999-SNAPSHOT
+
+ quarkus-reactive-transactions-deployment
+ Quarkus - Hibernate Reactive Transactions - Deployment
+
+
+
+ io.quarkus
+ quarkus-arc-deployment
+
+
+ io.quarkus
+ quarkus-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-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..9f3b4549fa36a
--- /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 = "quarkus-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/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/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
+
+ io.quarkus
+ quarkus-reactive-transactions
+
org.junit.jupiter
junit-jupiter
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 5bfdadd421390..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;
-
-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..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,5 +1,7 @@
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;
@@ -8,7 +10,7 @@
@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 bb3cc826d4ca4..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;
@@ -56,17 +57,18 @@ 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());
}
- // 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";
+ // 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 +80,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 +242,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 +256,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/WithSessionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java
index cd8de943a973a..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;
@@ -10,7 +13,7 @@
@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 6275bd3e2a42b..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;
@@ -10,13 +13,14 @@
@WithSessionOnDemand
@Interceptor
@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200)
-public class WithSessionOnDemandInterceptor extends AbstractUniInterceptor {
+public class WithSessionOnDemandInterceptor {
@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)) {
+
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..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
@@ -1,22 +1,37 @@
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;
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 class WithTransactionInterceptor {
@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/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/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandAndTransactionalSameMethodTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandAndTransactionalSameMethodTest.java
new file mode 100644
index 0000000000000..9e9421a64fd70
--- /dev/null
+++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandAndTransactionalSameMethodTest.java
@@ -0,0 +1,38 @@
+package io.quarkus.hibernate.reactive.panache.test.transaction;
+
+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/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandTest.java
new file mode 100644
index 0000000000000..437c9e9a9b0f8
--- /dev/null
+++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithSessionOnDemandTest.java
@@ -0,0 +1,65 @@
+package io.quarkus.hibernate.reactive.panache.test.transaction;
+
+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/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTest.java
new file mode 100644
index 0000000000000..cf1fbcb151a65
--- /dev/null
+++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTest.java
@@ -0,0 +1,65 @@
+package io.quarkus.hibernate.reactive.panache.test.transaction;
+
+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/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTransactionalSameMethodTest.java b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTransactionalSameMethodTest.java
new file mode 100644
index 0000000000000..b8fe37f68318b
--- /dev/null
+++ b/extensions/panache/hibernate-reactive-panache/deployment/src/test/java/io/quarkus/hibernate/reactive/panache/test/transaction/MixWithTransactionTransactionalSameMethodTest.java
@@ -0,0 +1,38 @@
+package io.quarkus.hibernate.reactive.panache.test.transaction;
+
+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/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..8e5dda92aebb6
--- /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
+
+ quarkus-reactive-transactions-integration-tests
+ Quarkus - Hibernate Reactive Transactions - Integration Tests
+
+
+ true
+
+
+
+
+ io.quarkus
+ quarkus-rest
+
+
+ io.quarkus
+ quarkus-reactive-transactions
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+
+
+
+
+
+ io.quarkus
+ quarkus-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..fafc711089490
--- /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("/quarkus-reactive-transactions")
+@ApplicationScoped
+public class HibernateReactiveTransactionsResource {
+ // add some rest methods here
+
+ @GET
+ public String hello() {
+ return "Hello quarkus-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..5163aebc8612c
--- /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("/quarkus-reactive-transactions")
+ .then()
+ .statusCode(200)
+ .body(is("Hello quarkus-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