diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java index a5f3b0b888..18d392ea60 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java @@ -285,7 +285,9 @@ public SpiTransaction wrapExternalConnection(Connection c) { private SpiTransaction createTransaction(TxScope txScope) { if (txScope.isReadonly()) { - return createReadOnlyTransaction(null, false); + // Honor isolation on read-only scopes (e.g. @Transactional(readOnly=true, isolation=...)) + SpiTransaction transaction = createReadOnlyTransaction(null, false); + return transactionFactory.setIsolationLevel(transaction, txScope.getIsolationLevel()); } else { return createTransaction(true, txScope.getIsolationLevel()); } diff --git a/ebean-test/src/test/java/org/tests/transaction/TestTransactionalReadOnly.java b/ebean-test/src/test/java/org/tests/transaction/TestTransactionalReadOnly.java index 9b20104eb2..800af58757 100644 --- a/ebean-test/src/test/java/org/tests/transaction/TestTransactionalReadOnly.java +++ b/ebean-test/src/test/java/org/tests/transaction/TestTransactionalReadOnly.java @@ -1,12 +1,17 @@ package org.tests.transaction; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; +import io.ebean.Transaction; +import io.ebean.TxScope; import io.ebean.annotation.Transactional; +import io.ebean.annotation.TxIsolation; import io.ebean.meta.MetaTimedMetric; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; import java.util.Optional; @@ -38,6 +43,33 @@ public void test_main_datasource() { assertThat(metric(timedMetrics, "txn.readonly")).isEmpty(); } + /** + * #3407 read-only TxScope must honor isolation (previously dropped for createReadOnlyTransaction). + */ + @Test + public void test_readonly_honors_isolation() throws SQLException { + TxScope scope = TxScope.required() + .setReadOnly(true) + .setIsolation(TxIsolation.SERIALIZABLE); + + DB.execute(scope, () -> { + Transaction txn = DB.currentTransaction(); + assertThat(txn).isNotNull(); + try { + assertThat(txn.connection().getTransactionIsolation()) + .isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + } catch (SQLException e) { + throw new RuntimeException(e); + } + DB.find(Customer.class).findCount(); + }); + } + + @Test + public void test_readonly_annotation_honors_isolation() throws SQLException { + executeTransactionalReadOnlyWithIsolation(); + } + private Optional metric(List timedMetrics, String name) { return timedMetrics.stream() .filter(metaTimedMetric -> metaTimedMetric.name().equals(name)) @@ -53,4 +85,13 @@ private void executeTransactionalUsingReadOnlyDataSource() { private void executeTransactionalUsingMainDataSource() { DB.find(Customer.class).findCount(); } + + @Transactional(readOnly = true, isolation = TxIsolation.SERIALIZABLE) + private void executeTransactionalReadOnlyWithIsolation() throws SQLException { + Transaction txn = DB.currentTransaction(); + assertThat(txn).isNotNull(); + assertThat(txn.connection().getTransactionIsolation()) + .isEqualTo(Connection.TRANSACTION_SERIALIZABLE); + DB.find(Customer.class).findCount(); + } }