Skip to content

Commit

Permalink
Provides read-only session
Browse files Browse the repository at this point in the history
  • Loading branch information
loicmathieu committed Nov 30, 2020
2 parents b2bc7c1 + 9711989 commit a92bfa5
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 0 deletions.
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.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;
}
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,16 @@ 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);
//TODO maybe set back the flush mode / default read only somewhere ?
}

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) {
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

0 comments on commit a92bfa5

Please sign in to comment.