diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/ReadOnlyTransactionTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/ReadOnlyTransactionTest.java new file mode 100644 index 00000000000000..586f545f5ab5d9 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/ReadOnlyTransactionTest.java @@ -0,0 +1,94 @@ +package io.quarkus.hibernate.orm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.transaction.Transactional; + +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.enhancer.Address; +import io.quarkus.hibernate.orm.runtime.SessionConfiguration; +import io.quarkus.test.QuarkusUnitTest; + +public class ReadOnlyTransactionTest { + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(Address.class) + .addAsResource("application.properties")); + + @Inject + EntityManager entityManager; + + @BeforeEach + @Transactional + void init() { + Address adr = new Address(); + adr.setStreet("rue de Paris"); + entityManager.persist(adr); + entityManager.flush(); + } + + @AfterEach + @Transactional + void destroy() { + int deleted = entityManager.createQuery("delete from Address where street = 'rue de Paris'").executeUpdate(); + assertEquals(1, deleted); + entityManager.flush(); + } + + @Test + @Transactional + @SessionConfiguration(readOnly = true) + public void testRO() { + TypedQuery
query = entityManager.createQuery("from Address where street = 'rue de Paris'", Address.class); + Address result = query.getSingleResult(); + assertNotNull(result); + + Session session = entityManager.unwrap(Session.class); + assertTrue(session.isDefaultReadOnly()); + assertEquals(FlushMode.MANUAL, session.getHibernateFlushMode()); + } + + @Test + @Transactional(Transactional.TxType.REQUIRES_NEW) + @SessionConfiguration(readOnly = true) + public void testSubTransactions() { + TypedQuery query = entityManager.createQuery("from Address where street = 'rue de Paris'", Address.class); + Address result = query.getSingleResult(); + assertNotNull(result); + + Session session = entityManager.unwrap(Session.class); + assertTrue(session.isDefaultReadOnly()); + assertEquals(FlushMode.MANUAL, session.getHibernateFlushMode()); + + // as it's a new transaction, it works + newTransaction(); + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + @SessionConfiguration(readOnly = false) + public void newTransaction() { + Session session = entityManager.unwrap(Session.class); + assertFalse(session.isDefaultReadOnly()); + assertEquals(FlushMode.AUTO, session.getHibernateFlushMode()); + + Address adr = new Address(); + adr.setStreet("rue du paradis"); + entityManager.persist(adr); + entityManager.flush(); + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/SessionConfiguration.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/SessionConfiguration.java new file mode 100644 index 00000000000000..a8a8170bf970b5 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/SessionConfiguration.java @@ -0,0 +1,26 @@ +package io.quarkus.hibernate.orm.runtime; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.quarkus.narayana.jta.runtime.AdditionalTransactionConfiguration; + +/** + * This annotation can be used to configure the Hibernate session. + */ +@Inherited +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(value = RetentionPolicy.RUNTIME) +@AdditionalTransactionConfiguration +public @interface SessionConfiguration { + /** + * Whether or not the transaction performs read only operations on the underlying transactional resource. + * Depending on the transactional resource, optimizations can be performed in case of read only transactions. + * + * @return true if read only. + */ + boolean readOnly() default false; +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/entitymanager/TransactionScopedEntityManager.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/entitymanager/TransactionScopedEntityManager.java index f91ce93e5fd6e7..889b9050020efa 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/entitymanager/TransactionScopedEntityManager.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/entitymanager/TransactionScopedEntityManager.java @@ -1,5 +1,6 @@ package io.quarkus.hibernate.orm.runtime.entitymanager; +import java.lang.annotation.Annotation; import java.util.List; import java.util.Map; @@ -24,7 +25,12 @@ import javax.transaction.TransactionManager; import javax.transaction.TransactionSynchronizationRegistry; +import org.hibernate.FlushMode; +import org.hibernate.Session; + import io.quarkus.hibernate.orm.runtime.RequestScopedEntityManagerHolder; +import io.quarkus.hibernate.orm.runtime.SessionConfiguration; +import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase; import io.quarkus.runtime.BlockingOperationControl; public class TransactionScopedEntityManager implements EntityManager { @@ -58,6 +64,16 @@ EntityManagerResult getEntityManager() { return new EntityManagerResult(entityManager, false, true); } EntityManager newEntityManager = entityManagerFactory.createEntityManager(); + Map