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, Annotation> additionalConfig = (Map, Annotation>) transactionSynchronizationRegistry + .getResource(TransactionalInterceptorBase.ADDITIONAL_CONFIG_KEY); + SessionConfiguration sessionConfiguration = (SessionConfiguration) additionalConfig.get(SessionConfiguration.class); + if (sessionConfiguration != null && sessionConfiguration.readOnly()) { + Session session = newEntityManager.unwrap(Session.class); + session.setDefaultReadOnly(true); + session.setHibernateFlushMode(FlushMode.MANUAL); + //TODO maybe set back the flush mode / default read only somewhere ? + } + newEntityManager.joinTransaction(); transactionSynchronizationRegistry.putResource(entityManagerKey, newEntityManager); transactionSynchronizationRegistry.registerInterposedSynchronization(new Synchronization() { diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/AdditionalTransactionConfiguration.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/AdditionalTransactionConfiguration.java new file mode 100644 index 00000000000000..4d6766c5a55d44 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/AdditionalTransactionConfiguration.java @@ -0,0 +1,16 @@ +package io.quarkus.narayana.jta.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; + +/** + * This is a meta annotation that indicates that the child annotation defines additional transactional configuration. + */ +@Inherited +@Target({ ElementType.ANNOTATION_TYPE }) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface AdditionalTransactionConfiguration { +} 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 ba67fde04a12a0..4b83f1d63e4d27 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 @@ -2,6 +2,8 @@ import java.io.Serializable; import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletionException; @@ -13,6 +15,7 @@ import javax.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; +import javax.transaction.TransactionSynchronizationRegistry; import javax.transaction.Transactional; import org.jboss.tm.usertx.client.ServerVMClientUserTransaction; @@ -21,6 +24,7 @@ import com.arjuna.ats.jta.logging.jtaLogger; import io.quarkus.arc.runtime.InterceptorBindings; +import io.quarkus.narayana.jta.runtime.AdditionalTransactionConfiguration; import io.quarkus.narayana.jta.runtime.CDIDelegatingTransactionManager; import io.quarkus.narayana.jta.runtime.TransactionConfiguration; import io.smallrye.mutiny.Multi; @@ -35,9 +39,14 @@ public abstract class TransactionalInterceptorBase implements Serializable { private static final long serialVersionUID = 1L; + public static final Object ADDITIONAL_CONFIG_KEY = new Object(); + @Inject TransactionManager transactionManager; + @Inject + TransactionSynchronizationRegistry transactionSynchronizationRegistry; + private final boolean userTransactionAvailable; protected TransactionalInterceptorBase(boolean userTransactionAvailable) { @@ -96,6 +105,35 @@ private TransactionConfiguration getTransactionConfiguration(InvocationContext i return configuration; } + private Map, Annotation> getAdditionalTransactionalConfiguration(InvocationContext ic) { + Map, Annotation> additionalTransactionConfigurations = new HashMap<>(); + + // Lookup annotations on the class + Class clazz; + Object target = ic.getTarget(); + if (target != null) { + clazz = target.getClass(); + } else { + // Very likely an intercepted static method + clazz = ic.getMethod().getDeclaringClass(); + } + for (Annotation annotation : clazz.getAnnotations()) { + if (annotation.annotationType().isAnnotationPresent(AdditionalTransactionConfiguration.class)) { + additionalTransactionConfigurations.put(annotation.annotationType(), annotation); + } + } + + // Lookup annotations on the method + // In case the same annotation type is defined both on the class and on the method, the method one will override the class one. + for (Annotation annotation : ic.getMethod().getAnnotations()) { + if (annotation.annotationType().isAnnotationPresent(AdditionalTransactionConfiguration.class)) { + additionalTransactionConfigurations.put(annotation.annotationType(), annotation); + } + } + + return additionalTransactionConfigurations; + } + protected Object invokeInOurTx(InvocationContext ic, TransactionManager tm) throws Exception { return invokeInOurTx(ic, tm, () -> { }); @@ -120,6 +158,10 @@ protected Object invokeInOurTx(InvocationContext ic, TransactionManager tm, Runn } } + Map, Annotation> additionalTransactionConfigurations = getAdditionalTransactionalConfiguration(ic); + // put the additional transaction configuration inside the synchronization registry to access it from Hibernate + transactionSynchronizationRegistry.putResource(ADDITIONAL_CONFIG_KEY, additionalTransactionConfigurations); + boolean throwing = false; Object ret = null;