Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: read only sessions #10077

Closed
wants to merge 2 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -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<Address> 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<Address> 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.hibernate.orm;

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;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.hibernate.orm.runtime.entitymanager;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;

Expand All @@ -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 {
Expand Down Expand Up @@ -58,6 +64,15 @@ EntityManagerResult getEntityManager() {
return new EntityManagerResult(entityManager, false, true);
}
EntityManager newEntityManager = entityManagerFactory.createEntityManager();
Map<Class<?>, Annotation> additionalConfig = (Map<Class<?>, 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);
}

newEntityManager.joinTransaction();
transactionSynchronizationRegistry.putResource(entityManagerKey, newEntityManager);
transactionSynchronizationRegistry.registerInterposedSynchronization(new Synchronization() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -96,6 +105,35 @@ private TransactionConfiguration getTransactionConfiguration(InvocationContext i
return configuration;
}

private Map<Class<?>, Annotation> getAdditionalTransactionalConfiguration(InvocationContext ic) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be cached, as it is slow. Ideally we would pre-compute this at build time.

Also does this work in native mode? I think it will but an integration test would be helpful.

Map<Class<?>, 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, () -> {
});
Expand All @@ -120,6 +158,10 @@ protected Object invokeInOurTx(InvocationContext ic, TransactionManager tm, Runn
}
}

Map<Class<?>, 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;

Expand Down