Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4565,13 +4565,6 @@
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>${kafka.version}</version>
<!-- TODO: remove the exclusion after upgrading to kafka-clients depending on the new lz4-java groupId (at.yawk.lz4) -->
<exclusions>
<exclusion>
<groupId>org.lz4</groupId>
<artifactId>lz4-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>at.yawk.lz4</groupId>
Expand Down
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/hibernate-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ Mixing both transaction management styles in the same reactive pipeline is not s
In the future, we'll deprecate the previous annotations provided by Panache and and support only `@Transactional`.

You can inject either `Mutiny.Session` or `Mutiny.StatelessSession`.
Be careful of injecting both session types in the same bean. Attempting to use both session types within the same transaction will throw an `IllegalStateException`.
Mixing both session types in the same transaction should work, but should be reserved for exotic use cases implemented by advanced users, as the (stateful) session will not be aware of changes operated through the stateless session, which could thus conflict or be silently erased by (stateful) session writes.

[[transactional-different-pipelines]]
=== Using Declarative Transaction Management in different reactive pipelines
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.quarkus.hibernate.orm.stateless;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.List;

import jakarta.inject.Inject;

import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.orm.MyEntity;
import io.quarkus.narayana.jta.QuarkusTransaction;
import io.quarkus.test.QuarkusExtensionTest;

/**
* Verifies that Hibernate ORM allows mixing regular Session and StatelessSession
* within the same transaction.
*/
public class MixStatelessStatefulSessionTest {

@RegisterExtension
static QuarkusExtensionTest runner = new QuarkusExtensionTest()
.withApplicationRoot((jar) -> jar
.addClass(MyEntity.class))
.withConfigurationResource("application.properties");

@Inject
Session session;

@Inject
StatelessSession statelessSession;

@Test
public void testRegularSessionThenStatelessSessionInSameTransaction() {
// Use regular Session, then StatelessSession in same transaction
QuarkusTransaction.requiringNew().run(() -> {
MyEntity entity = new MyEntity("testRegular");
session.persist(entity);
session.flush();
// Now use StatelessSession in the same transaction - should work without error
List<String> list = statelessSession
.createSelectionQuery("SELECT e.name from MyEntity e WHERE e.name = 'testRegular'", String.class)
.getResultList();
assertThat(list).containsOnly("testRegular");
});

// Verify it was persisted
QuarkusTransaction.requiringNew().run(() -> {
long count = session
.createSelectionQuery("SELECT count(e) from MyEntity e WHERE e.name = 'testRegular'", Long.class)
.getSingleResult();
assertThat(count).isEqualTo(1);
});
}

@Test
public void testStatelessSessionThenRegularSessionInSameTransaction() {
// Use StatelessSession, then regular Session in same transaction
QuarkusTransaction.requiringNew().run(() -> {
MyEntity entity = new MyEntity("testStateless");
statelessSession.insert(entity);
// Now use regular Session in the same transaction - should work without error
List<String> list = session
.createSelectionQuery("SELECT e.name from MyEntity e WHERE e.name = 'testStateless'", String.class)
.getResultList();
assertThat(list).containsOnly("testStateless");
});

// Verify it was persisted
QuarkusTransaction.requiringNew().run(() -> {
long count = session
.createSelectionQuery("SELECT count(e) from MyEntity e WHERE e.name = 'testStateless'", Long.class)
.getSingleResult();
assertThat(count).isEqualTo(1);
});
}

@Test
public void testBothSessionsCommitTogether() {
// Make changes via both sessions, verify both commit together
QuarkusTransaction.requiringNew().run(() -> {
MyEntity entity1 = new MyEntity("commitViaRegular");
session.persist(entity1);
session.flush();

MyEntity entity2 = new MyEntity("commitViaStateless");
statelessSession.insert(entity2);
});

// Verify both were persisted
QuarkusTransaction.requiringNew().run(() -> {
long count1 = session
.createSelectionQuery("SELECT count(e) from MyEntity e WHERE e.name = 'commitViaRegular'", Long.class)
.getSingleResult();
long count2 = session
.createSelectionQuery("SELECT count(e) from MyEntity e WHERE e.name = 'commitViaStateless'", Long.class)
.getSingleResult();
assertThat(count1).isEqualTo(1);
assertThat(count2).isEqualTo(1);
});
}

@Test
public void testBothSessionsRollbackTogether() {
// Make changes via both sessions, then rollback - verify both rolled back
assertThatThrownBy(() -> {
QuarkusTransaction.requiringNew().run(() -> {
MyEntity entity1 = new MyEntity("rollbackViaRegular");
session.persist(entity1);
session.flush();

MyEntity entity2 = new MyEntity("rollbackViaStateless");
statelessSession.insert(entity2);

throw new RuntimeException("Force rollback");
});
}).isInstanceOf(RuntimeException.class)
.hasMessage("Force rollback");

// Verify neither was persisted
QuarkusTransaction.requiringNew().run(() -> {
long count1 = session
.createSelectionQuery("SELECT count(e) from MyEntity e WHERE e.name = 'rollbackViaRegular'", Long.class)
.getSingleResult();
long count2 = session
.createSelectionQuery("SELECT count(e) from MyEntity e WHERE e.name = 'rollbackViaStateless'", Long.class)
.getSingleResult();
assertThat(count1).isEqualTo(0);
assertThat(count2).isEqualTo(0);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.reactive.transaction.TransactionalInterceptorRequired;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorRequired;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.vertx.RunOnVertxContext;
import io.quarkus.test.vertx.UniAsserter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.reactive.transaction.TransactionalInterceptorRequired;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorRequired;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.vertx.RunOnVertxContext;
import io.quarkus.test.vertx.UniAsserter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.hibernate.reactive.runtime.OpenedSessionsState;
import io.quarkus.reactive.transaction.TransactionalInterceptorRequired;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorRequired;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.vertx.RunOnVertxContext;
import io.quarkus.test.vertx.UniAsserter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.reactive.transaction.TransactionalInterceptorMandatory;
import io.quarkus.reactive.transaction.TransactionalInterceptorNever;
import io.quarkus.reactive.transaction.TransactionalInterceptorNotSupported;
import io.quarkus.reactive.transaction.TransactionalInterceptorRequiresNew;
import io.quarkus.reactive.transaction.TransactionalInterceptorSupports;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorMandatory;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorNever;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorNotSupported;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorRequiresNew;
import io.quarkus.reactive.transaction.runtime.TransactionalInterceptorSupports;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.vertx.RunOnVertxContext;
import io.smallrye.mutiny.Uni;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,56 +14,118 @@
import io.quarkus.test.vertx.RunOnVertxContext;
import io.quarkus.test.vertx.UniAsserter;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.sqlclient.Pool;

/**
* Verifies that Hibernate Reactive allows mixing regular Session and StatelessSession
* within the same transaction.
*/
public class MixStatelessStatefulSessionTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(jar -> jar.addClasses(Hero.class))
.withConfigurationResource("application.properties");
.withConfigurationResource("application-reactive-transaction.properties");

@Inject
Mutiny.Session session;

@Inject
Mutiny.StatelessSession statelessSession;

@Inject
Pool pool;

@Test
@RunOnVertxContext
public void testRegularSessionThenStatelessSessionInTransactional(UniAsserter asserter) {
// Use regular Session, then StatelessSession in same transaction - should work without error
asserter.execute(() -> assertThat(pool.size()).isEqualTo(1));
asserter.assertThat(
() -> transactionalMethodUsingRegularSessionThenStatelessSession(),
count -> assertThat(count).isNotNull());
// Verify pool size is still 1 (connection was shared and released)
asserter.execute(() -> assertThat(pool.size()).isEqualTo(1));
}

@Test
@RunOnVertxContext
public void testStatelessSessionThenRegularSessionInTransactional(UniAsserter asserter) {
asserter.assertFailedWith(
// Use StatelessSession, then regular Session in same transaction - should work without error
asserter.execute(() -> assertThat(pool.size()).isEqualTo(1));
asserter.assertThat(
() -> transactionalMethodUsingStatelessSessionThenRegularSession(),
e -> assertThat(e)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("stateless session for the same Persistence Unit is already opened")
.hasMessageContaining(
"Mixing different kinds of sessions in the same transaction is not supported yet"));
count -> assertThat(count).isNotNull());
// Verify pool size is still 1 (connection was shared and released)
asserter.execute(() -> assertThat(pool.size()).isEqualTo(1));
}

@Test
@RunOnVertxContext
public void testRegularSessionThenStatelessSessionInTransactional(UniAsserter asserter) {
public void testBothSessionsCommitTogether(UniAsserter asserter) {
// Make changes via both sessions, verify both commit together
asserter.assertThat(
() -> transactionalMethodCreatingViaBothSessions("CommitHero1", "CommitHero2"),
v -> assertThat(v).isNull());

// Verify both were persisted
asserter.assertThat(
() -> transactionalMethodCountingHeroes("CommitHero%"),
count -> assertThat(count).isEqualTo(2L));
}

@Test
@RunOnVertxContext
public void testBothSessionsRollbackTogether(UniAsserter asserter) {
// Make changes via both sessions, then throw exception - verify both rolled back
asserter.assertFailedWith(
() -> transactionalMethodUsingRegularSessionThenStatelessSession(),
() -> transactionalMethodCreatingViaBothSessionsThenFailing("RollbackHero1", "RollbackHero2"),
e -> assertThat(e)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("session for the same Persistence Unit is already opened")
.hasMessageContaining(
"Mixing different kinds of sessions in the same transaction is not supported yet"));
.isInstanceOf(RuntimeException.class)
.hasMessage("Force rollback"));

// Verify neither was persisted
asserter.assertThat(
() -> transactionalMethodCountingHeroes("RollbackHero%"),
count -> assertThat(count).isEqualTo(0L));
}

@Transactional
public Uni<Object> transactionalMethodUsingRegularSessionThenStatelessSession() {
return session.createQuery("select count(1) from Hero h", Long.class).getSingleResult()
.chain(count -> statelessSession.createQuery("select count(1) from Hero h", Long.class)
public Uni<Long> transactionalMethodUsingRegularSessionThenStatelessSession() {
return session.createSelectionQuery("select count(h) from Hero h", Long.class).getSingleResult()
.chain(count -> statelessSession.createSelectionQuery("select count(h) from Hero h", Long.class)
.getSingleResult());
}

@Transactional
public Uni<Object> transactionalMethodUsingStatelessSessionThenRegularSession() {
return statelessSession.createQuery("select count(1) from Hero h", Long.class).getSingleResult()
.chain(count -> session.createQuery("select count(1) from Hero h", Long.class)
public Uni<Long> transactionalMethodUsingStatelessSessionThenRegularSession() {
return statelessSession.createSelectionQuery("select count(h) from Hero h", Long.class).getSingleResult()
.chain(count -> session.createSelectionQuery("select count(h) from Hero h", Long.class)
.getSingleResult());
}

@Transactional
public Uni<Void> transactionalMethodCreatingViaBothSessions(String name1, String name2) {
Hero hero1 = new Hero(name1);
return session.persist(hero1)
.call(() -> session.flush())
.chain(() -> {
Hero hero2 = new Hero(name2);
return statelessSession.insert(hero2);
});
}

@Transactional
public Uni<Void> transactionalMethodCreatingViaBothSessionsThenFailing(String name1, String name2) {
return transactionalMethodCreatingViaBothSessions(name1, name2)
.chain(() -> Uni.createFrom().failure(new RuntimeException("Force rollback")));
}

@Transactional
public Uni<Long> transactionalMethodCountingHeroes(String namePattern) {
return session.createSelectionQuery("select count(h) from Hero h where h.name like :name", Long.class)
.setParameter("name", namePattern)
.getSingleResult();
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
quarkus.datasource.db-kind=postgresql
quarkus.datasource.reactive=true
# Using max-size=1 ensures that:
# 1. Connection management is tested properly (connection must be released after transaction)
# 2. Session types sharing the same connection is verified (tests would hang if separate connections were used)
quarkus.datasource.reactive.max-size=1

quarkus.hibernate-orm.log.sql=true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package io.quarkus.hibernate.reactive.runtime;

import static io.quarkus.reactive.transaction.TransactionalInterceptorBase.PERSISTENCE_UNIT_NAME_KEY;
import static io.quarkus.reactive.transaction.TransactionalInterceptorBase.TRANSACTIONAL_METHOD_KEY;
import static io.quarkus.reactive.transaction.runtime.TransactionalInterceptorBase.PERSISTENCE_UNIT_NAME_KEY;
import static io.quarkus.reactive.transaction.runtime.TransactionalInterceptorBase.TRANSACTIONAL_METHOD_KEY;

import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -134,17 +134,6 @@ public static Mutiny.Session getSession(String persistenceUnitName) {
} else if (context.getLocal(TRANSACTIONAL_METHOD_KEY) == null) {
throw new IllegalStateException(noSessionFoundErrorMessage());
} else {

Optional<OpenedSessionsState.SessionWithKey<Mutiny.StatelessSession>> openedStatelessSession = OPENED_SESSIONS_STATE_STATELESS
.getOpenedSession(
context,
persistenceUnitName);

if (openedStatelessSession.isPresent()) {
throw new IllegalStateException("A stateless session for the same Persistence Unit is already opened."
+ "\n\t- Mixing different kinds of sessions in the same transaction is not supported yet.");
}

// Store the persistence unit name so that we can close only this session at the end of the interceptor
context.putLocal(PERSISTENCE_UNIT_NAME_KEY, persistenceUnitName);

Expand Down Expand Up @@ -179,17 +168,6 @@ public static Mutiny.StatelessSession getStatelessSession(String persistenceUnit
} else if (context.getLocal(TRANSACTIONAL_METHOD_KEY) == null) {
throw new IllegalStateException(noSessionFoundErrorMessage());
} else {

Optional<OpenedSessionsState.SessionWithKey<Mutiny.Session>> openedRegularSession = OPENED_SESSIONS_STATE
.getOpenedSession(
context,
persistenceUnitName);

if (openedRegularSession.isPresent()) {
throw new IllegalStateException("A (non-stateless) session for the same Persistence Unit is already opened."
+ "\n\t- Mixing different kinds of sessions in the same transaction is not supported yet.");
}

// Store the persistence unit name so that we can close only this session at the end of the interceptor
context.putLocal(PERSISTENCE_UNIT_NAME_KEY, persistenceUnitName);

Expand Down
Loading
Loading