diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java index ad507f61870d4..6fdaa129aae6e 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/ClassNames.java @@ -51,6 +51,7 @@ private static DotName createConstant(String fqcn) { public static final DotName SESSION_FACTORY = createConstant("org.hibernate.SessionFactory"); public static final DotName ENTITY_MANAGER = createConstant("jakarta.persistence.EntityManager"); public static final DotName SESSION = createConstant("org.hibernate.Session"); + public static final DotName STATELESS_SESSION = createConstant("org.hibernate.StatelessSession"); public static final DotName INTERCEPTOR = createConstant("org.hibernate.Interceptor"); public static final DotName STATEMENT_INSPECTOR = createConstant("org.hibernate.resource.jdbc.spi.StatementInspector"); diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java index 914116d1f6e78..9bdb8da111bd0 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmCdiProcessor.java @@ -11,6 +11,7 @@ import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassType; @@ -47,6 +48,7 @@ public class HibernateOrmCdiProcessor { private static final List SESSION_FACTORY_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER_FACTORY, ClassNames.SESSION_FACTORY); private static final List SESSION_EXPOSED_TYPES = Arrays.asList(ClassNames.ENTITY_MANAGER, ClassNames.SESSION); + private static final List STATELESS_SESSION_EXPOSED_TYPES = List.of(ClassNames.STATELESS_SESSION); private static final Set PERSISTENCE_UNIT_EXTENSION_VALID_TYPES = Set.of( ClassNames.TENANT_RESOLVER, @@ -148,6 +150,15 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder, .createWith(recorder.sessionSupplier(persistenceUnitName)) .addInjectionPoint(ClassType.create(DotName.createSimple(TransactionSessions.class))) .done()); + + // same for StatelessSession + syntheticBeanBuildItemBuildProducer + .produce(createSyntheticBean(persistenceUnitName, + true, true, + StatelessSession.class, STATELESS_SESSION_EXPOSED_TYPES, false) + .createWith(recorder.statelessSessionSupplier(persistenceUnitName)) + .addInjectionPoint(ClassType.create(DotName.createSimple(TransactionSessions.class))) + .done()); } return; } @@ -179,6 +190,15 @@ void generateDataSourceBeans(HibernateOrmRecorder recorder, .createWith(recorder.sessionSupplier(persistenceUnitName)) .addInjectionPoint(ClassType.create(DotName.createSimple(TransactionSessions.class))) .done()); + + // same for StatelessSession + syntheticBeanBuildItemBuildProducer + .produce(createSyntheticBean(persistenceUnitName, + true, true, + StatelessSession.class, STATELESS_SESSION_EXPOSED_TYPES, false) + .createWith(recorder.statelessSessionSupplier(persistenceUnitName)) + .addInjectionPoint(ClassType.create(DotName.createSimple(TransactionSessions.class))) + .done()); } } } diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 5ed792946a02f..ca2614b2c4478 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -140,6 +140,7 @@ import io.quarkus.hibernate.orm.runtime.JPAConfig; import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil; import io.quarkus.hibernate.orm.runtime.RequestScopedSessionHolder; +import io.quarkus.hibernate.orm.runtime.RequestScopedStatelessSessionHolder; import io.quarkus.hibernate.orm.runtime.TransactionSessions; import io.quarkus.hibernate.orm.runtime.boot.QuarkusPersistenceUnitDefinition; import io.quarkus.hibernate.orm.runtime.boot.scan.QuarkusScanner; @@ -674,6 +675,7 @@ void registerBeans(HibernateOrmConfig hibernateOrmConfig, unremovableClasses.add(TransactionSessions.class); } unremovableClasses.add(RequestScopedSessionHolder.class); + unremovableClasses.add(RequestScopedStatelessSessionHolder.class); unremovableClasses.add(QuarkusArcBeanContainer.class); additionalBeans.produce(AdditionalBeanBuildItem.builder().setUnremovable() diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/stateless/StatelessSessionWithinRequestScopeTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/stateless/StatelessSessionWithinRequestScopeTest.java new file mode 100644 index 0000000000000..04872f3c258b3 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/stateless/StatelessSessionWithinRequestScopeTest.java @@ -0,0 +1,46 @@ +package io.quarkus.hibernate.orm.stateless; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.hibernate.StatelessSession; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +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.arc.Arc; +import io.quarkus.hibernate.orm.MyEntity; +import io.quarkus.hibernate.orm.naming.PrefixPhysicalNamingStrategy; +import io.quarkus.test.QuarkusUnitTest; + +public class StatelessSessionWithinRequestScopeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyEntity.class, PrefixPhysicalNamingStrategy.class) + .addAsResource(EmptyAsset.INSTANCE, "import.sql") + .addAsResource("application-physical-naming-strategy.properties", "application.properties")); + + @Inject + StatelessSession statelessSession; + + @BeforeEach + public void activateRequestContext() { + Arc.container().requestContext().activate(); + } + + @Test + public void test() throws Exception { + Number result = (Number) statelessSession.createNativeQuery("SELECT COUNT(*) FROM TBL_MYENTITY").getSingleResult(); + assertEquals(0, result.intValue()); + } + + @AfterEach + public void terminateRequestContext() { + Arc.container().requestContext().terminate(); + } +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/stateless/StatelessSessionWithinTransactionTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/stateless/StatelessSessionWithinTransactionTest.java new file mode 100644 index 0000000000000..a9fd1fa62bdc9 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/stateless/StatelessSessionWithinTransactionTest.java @@ -0,0 +1,47 @@ +package io.quarkus.hibernate.orm.stateless; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.transaction.UserTransaction; + +import org.hibernate.Session; +import org.hibernate.StatelessSession; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.hibernate.orm.enhancer.Address; +import io.quarkus.test.QuarkusUnitTest; + +public class StatelessSessionWithinTransactionTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClass(Address.class)) + .withConfigurationResource("application.properties"); + + @Inject + StatelessSession statelessSession; + + @Inject + Session session; + + @Inject + UserTransaction transaction; + + @Test + public void test() throws Exception { + transaction.begin(); + Address entity = new Address("high street"); + session.persist(entity); + transaction.commit(); + + transaction.begin(); + List list = statelessSession.createQuery("SELECT street from Address").getResultList(); + assertThat(list).containsOnly("high street"); + transaction.commit(); + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java index 756dfa8664bd9..dad1fea1417a1 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/HibernateOrmRecorder.java @@ -14,6 +14,7 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; import org.hibernate.boot.archive.scan.spi.Scanner; import org.hibernate.engine.spi.SessionLazyDelegator; import org.hibernate.integrator.spi.Integrator; @@ -126,6 +127,23 @@ public Session get() { }; } + public Function, StatelessSession> statelessSessionSupplier( + String persistenceUnitName) { + return new Function, StatelessSession>() { + + @Override + public StatelessSession apply(SyntheticCreationalContext context) { + TransactionSessions transactionSessions = context.getInjectedReference(TransactionSessions.class); + return new StatelessSessionLazyDelegator(new Supplier() { + @Override + public StatelessSession get() { + return transactionSessions.getStatelessSession(persistenceUnitName); + } + }); + } + }; + } + public void doValidation(String puName) { Optional val; if (puName.equals(PersistenceUnitUtil.DEFAULT_PERSISTENCE_UNIT_NAME)) { diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/RequestScopedStatelessSessionHolder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/RequestScopedStatelessSessionHolder.java new file mode 100644 index 0000000000000..600d2dc3f93c6 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/RequestScopedStatelessSessionHolder.java @@ -0,0 +1,31 @@ +package io.quarkus.hibernate.orm.runtime; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.RequestScoped; + +import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; + +/** + * Bean that is used to manage request scoped stateless sessions + */ +@RequestScoped +public class RequestScopedStatelessSessionHolder { + + private final Map sessions = new HashMap<>(); + + public StatelessSession getOrCreateSession(String name, SessionFactory factory) { + return sessions.computeIfAbsent(name, (n) -> factory.openStatelessSession()); + } + + @PreDestroy + public void destroy() { + for (Map.Entry entry : sessions.entrySet()) { + entry.getValue().close(); + } + } + +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java new file mode 100644 index 0000000000000..d9ccac4fec5a6 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/StatelessSessionLazyDelegator.java @@ -0,0 +1,352 @@ +package io.quarkus.hibernate.orm.runtime; + +import java.util.function.Supplier; + +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; + +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.StatelessSession; +import org.hibernate.Transaction; +import org.hibernate.jdbc.ReturningWork; +import org.hibernate.jdbc.Work; +import org.hibernate.procedure.ProcedureCall; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.Query; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsertSelect; + +/** + * Plays the exact same role as {@link org.hibernate.engine.spi.SessionLazyDelegator} for {@link org.hibernate.Session} + */ +class StatelessSessionLazyDelegator implements StatelessSession { + + private final Supplier delegate; + + public StatelessSessionLazyDelegator(Supplier delegate) { + this.delegate = delegate; + } + + @Override + public void close() { + delegate.get().close(); + } + + @Override + public Object insert(Object entity) { + return delegate.get().insert(entity); + } + + @Override + public Object insert(String entityName, Object entity) { + return delegate.get().insert(entityName, entity); + } + + @Override + public void update(Object entity) { + delegate.get().update(entity); + } + + @Override + public void update(String entityName, Object entity) { + delegate.get().update(entityName, entity); + } + + @Override + public void delete(Object entity) { + delegate.get().delete(entity); + } + + @Override + public void delete(String entityName, Object entity) { + delegate.get().delete(entityName, entity); + } + + @Override + public Object get(String entityName, Object id) { + return delegate.get().get(entityName, id); + } + + @Override + public T get(Class entityClass, Object id) { + return delegate.get().get(entityClass, id); + } + + @Override + public Object get(String entityName, Object id, LockMode lockMode) { + return delegate.get().get(entityName, id, lockMode); + } + + @Override + public T get(Class entityClass, Object id, LockMode lockMode) { + return delegate.get().get(entityClass, id, lockMode); + } + + @Override + public void refresh(Object entity) { + delegate.get().refresh(entity); + } + + @Override + public void refresh(String entityName, Object entity) { + delegate.get().refresh(entityName, entity); + } + + @Override + public void refresh(Object entity, LockMode lockMode) { + delegate.get().refresh(entity, lockMode); + } + + @Override + public void refresh(String entityName, Object entity, LockMode lockMode) { + delegate.get().refresh(entityName, entity, lockMode); + } + + @Override + public void fetch(Object association) { + delegate.get().fetch(association); + } + + @Override + public String getTenantIdentifier() { + return delegate.get().getTenantIdentifier(); + } + + @Override + public boolean isOpen() { + return delegate.get().isOpen(); + } + + @Override + public boolean isConnected() { + return delegate.get().isConnected(); + } + + @Override + public Transaction beginTransaction() { + return delegate.get().beginTransaction(); + } + + @Override + public Transaction getTransaction() { + return delegate.get().getTransaction(); + } + + @Override + public void joinTransaction() { + delegate.get().joinTransaction(); + } + + @Override + public boolean isJoinedToTransaction() { + return delegate.get().isJoinedToTransaction(); + } + + @Override + public ProcedureCall getNamedProcedureCall(String name) { + return delegate.get().getNamedProcedureCall(name); + } + + @Override + public ProcedureCall createStoredProcedureCall(String procedureName) { + return delegate.get().createStoredProcedureCall(procedureName); + } + + @Override + public ProcedureCall createStoredProcedureCall(String procedureName, Class... resultClasses) { + return delegate.get().createStoredProcedureCall(procedureName, resultClasses); + } + + @Override + public ProcedureCall createStoredProcedureCall(String procedureName, String... resultSetMappings) { + return delegate.get().createStoredProcedureCall(procedureName, resultSetMappings); + } + + @Override + public ProcedureCall createNamedStoredProcedureQuery(String name) { + return delegate.get().createNamedStoredProcedureQuery(name); + } + + @Override + public ProcedureCall createStoredProcedureQuery(String procedureName) { + return delegate.get().createStoredProcedureQuery(procedureName); + } + + @Override + public ProcedureCall createStoredProcedureQuery(String procedureName, Class... resultClasses) { + return delegate.get().createStoredProcedureQuery(procedureName, resultClasses); + } + + @Override + public ProcedureCall createStoredProcedureQuery(String procedureName, String... resultSetMappings) { + return delegate.get().createStoredProcedureQuery(procedureName, resultSetMappings); + } + + @Override + public Integer getJdbcBatchSize() { + return delegate.get().getJdbcBatchSize(); + } + + @Override + public void setJdbcBatchSize(Integer jdbcBatchSize) { + delegate.get().setJdbcBatchSize(jdbcBatchSize); + } + + @Override + public HibernateCriteriaBuilder getCriteriaBuilder() { + return delegate.get().getCriteriaBuilder(); + } + + @Override + public void doWork(Work work) throws HibernateException { + delegate.get().doWork(work); + } + + @Override + public T doReturningWork(ReturningWork work) throws HibernateException { + return delegate.get().doReturningWork(work); + } + + @Override + @Deprecated(since = "6.0") + public Query createQuery(String queryString) { + return delegate.get().createQuery(queryString); + } + + @Override + public Query createQuery(String queryString, Class resultClass) { + return delegate.get().createQuery(queryString, resultClass); + } + + @Override + public Query createQuery(CriteriaQuery criteriaQuery) { + return delegate.get().createQuery(criteriaQuery); + } + + @Override + @Deprecated(since = "6.0") + public Query createQuery(CriteriaUpdate updateQuery) { + return delegate.get().createQuery(updateQuery); + } + + @Override + @Deprecated(since = "6.0") + public Query createQuery(CriteriaDelete deleteQuery) { + return delegate.get().createQuery(deleteQuery); + } + + @Override + @Deprecated(since = "6.0") + public NativeQuery createNativeQuery(String sqlString) { + return delegate.get().createNativeQuery(sqlString); + } + + @Override + public NativeQuery createNativeQuery(String sqlString, Class resultClass) { + return delegate.get().createNativeQuery(sqlString, resultClass); + } + + @Override + public NativeQuery createNativeQuery(String sqlString, Class resultClass, String tableAlias) { + return delegate.get().createNativeQuery(sqlString, resultClass, tableAlias); + } + + @Override + @Deprecated(since = "6.0") + public NativeQuery createNativeQuery(String sqlString, String resultSetMappingName) { + return delegate.get().createNativeQuery(sqlString, resultSetMappingName); + } + + @Override + public NativeQuery createNativeQuery(String sqlString, String resultSetMappingName, Class resultClass) { + return delegate.get().createNativeQuery(sqlString, resultSetMappingName, resultClass); + } + + @Override + public SelectionQuery createSelectionQuery(String hqlString) { + return delegate.get().createSelectionQuery(hqlString); + } + + @Override + public SelectionQuery createSelectionQuery(String hqlString, Class resultType) { + return delegate.get().createSelectionQuery(hqlString, resultType); + } + + @Override + public SelectionQuery createSelectionQuery(CriteriaQuery criteria) { + return delegate.get().createSelectionQuery(criteria); + } + + @Override + public MutationQuery createMutationQuery(String hqlString) { + return delegate.get().createMutationQuery(hqlString); + } + + @Override + public MutationQuery createMutationQuery(CriteriaUpdate updateQuery) { + return delegate.get().createMutationQuery(updateQuery); + } + + @Override + public MutationQuery createMutationQuery(CriteriaDelete deleteQuery) { + return delegate.get().createMutationQuery(deleteQuery); + } + + @Override + public MutationQuery createMutationQuery(JpaCriteriaInsertSelect insertSelect) { + return delegate.get().createMutationQuery(insertSelect); + } + + @Override + public MutationQuery createNativeMutationQuery(String sqlString) { + return delegate.get().createNativeMutationQuery(sqlString); + } + + @Override + @Deprecated(since = "6.0") + public Query createNamedQuery(String name) { + return delegate.get().createNamedQuery(name); + } + + @Override + public Query createNamedQuery(String name, Class resultClass) { + return delegate.get().createNamedQuery(name, resultClass); + } + + @Override + public SelectionQuery createNamedSelectionQuery(String name) { + return delegate.get().createNamedSelectionQuery(name); + } + + @Override + public SelectionQuery createNamedSelectionQuery(String name, Class resultType) { + return delegate.get().createNamedSelectionQuery(name, resultType); + } + + @Override + public MutationQuery createNamedMutationQuery(String name) { + return delegate.get().createNamedMutationQuery(name); + } + + @Override + @Deprecated(since = "6.0") + public Query getNamedQuery(String queryName) { + return delegate.get().getNamedQuery(queryName); + } + + @Override + @Deprecated(since = "6.0") + public NativeQuery getNamedNativeQuery(String name) { + return delegate.get().getNamedNativeQuery(name); + } + + @Override + @Deprecated(since = "6.0") + public NativeQuery getNamedNativeQuery(String name, String resultSetMapping) { + return delegate.get().getNamedNativeQuery(name, resultSetMapping); + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/TransactionSessions.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/TransactionSessions.java index 22ae50d19590b..6d7bea5a538b0 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/TransactionSessions.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/TransactionSessions.java @@ -11,9 +11,11 @@ import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; import io.quarkus.arc.Arc; import io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession; +import io.quarkus.hibernate.orm.runtime.session.TransactionScopedStatelessSession; @ApplicationScoped public class TransactionSessions { @@ -24,10 +26,15 @@ public class TransactionSessions { @Inject Instance requestScopedSession; + @Inject + Instance requestScopedStatelessSession; + private final ConcurrentMap sessions; + private final ConcurrentMap staleSessions; public TransactionSessions() { this.sessions = new ConcurrentHashMap<>(); + this.staleSessions = new ConcurrentHashMap<>(); } public Session getSession(String unitName) { @@ -41,6 +48,17 @@ public Session getSession(String unitName) { requestScopedSession)); } + public StatelessSession getStatelessSession(String unitName) { + TransactionScopedStatelessSession session = staleSessions.get(unitName); + if (session != null) { + return session; + } + return staleSessions.computeIfAbsent(unitName, (un) -> new TransactionScopedStatelessSession( + getTransactionManager(), getTransactionSynchronizationRegistry(), + jpaConfig.getEntityManagerFactory(un).unwrap(SessionFactory.class), un, + requestScopedStatelessSession)); + } + private TransactionManager getTransactionManager() { return Arc.container() .instance(TransactionManager.class).get(); diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/JTAStatelessSessionOpener.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/JTAStatelessSessionOpener.java new file mode 100644 index 0000000000000..706aa5db2a5a9 --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/JTAStatelessSessionOpener.java @@ -0,0 +1,45 @@ +package io.quarkus.hibernate.orm.runtime.session; + +import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; +import org.hibernate.StatelessSessionBuilder; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +/** + * A delegate for opening a JTA-enabled Hibernate ORM StatelessSession. + *

+ * The main purpose of this class is to cache session options when possible; + * if we didn't care about caching, we could just replace any call to + */ +public class JTAStatelessSessionOpener { + public static JTAStatelessSessionOpener create(SessionFactory sessionFactory) { + final CurrentTenantIdentifierResolver currentTenantIdentifierResolver = sessionFactory + .unwrap(SessionFactoryImplementor.class).getCurrentTenantIdentifierResolver(); + if (currentTenantIdentifierResolver == null) { + // No tenant ID resolver: we can cache the options. + return new JTAStatelessSessionOpener(sessionFactory, createOptions(sessionFactory)); + } else { + // There is a tenant ID resolver: we cannot cache the options. + return new JTAStatelessSessionOpener(sessionFactory, null); + } + } + + private static StatelessSessionBuilder createOptions(SessionFactory sessionFactory) { + // TODO: what options would we pass here? + return sessionFactory.withStatelessOptions(); + } + + private final SessionFactory sessionFactory; + private final StatelessSessionBuilder cachedOptions; + + public JTAStatelessSessionOpener(SessionFactory sessionFactory, StatelessSessionBuilder cachedOptions) { + this.sessionFactory = sessionFactory; + this.cachedOptions = cachedOptions; + } + + public StatelessSession openSession() { + StatelessSessionBuilder options = cachedOptions != null ? cachedOptions : createOptions(sessionFactory); + return options.openStatelessSession(); + } +} diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java new file mode 100644 index 0000000000000..00ba865a6f07e --- /dev/null +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/session/TransactionScopedStatelessSession.java @@ -0,0 +1,627 @@ +package io.quarkus.hibernate.orm.runtime.session; + +import jakarta.enterprise.context.ContextNotActiveException; +import jakarta.enterprise.inject.Instance; +import jakarta.persistence.TransactionRequiredException; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.transaction.Status; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import org.hibernate.HibernateException; +import org.hibernate.LockMode; +import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; +import org.hibernate.Transaction; +import org.hibernate.jdbc.ReturningWork; +import org.hibernate.jdbc.Work; +import org.hibernate.procedure.ProcedureCall; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.Query; +import org.hibernate.query.SelectionQuery; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsertSelect; + +import io.quarkus.arc.Arc; +import io.quarkus.hibernate.orm.runtime.RequestScopedStatelessSessionHolder; +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.runtime.BlockingOperationNotAllowedException; + +public class TransactionScopedStatelessSession implements StatelessSession { + + protected static final String TRANSACTION_IS_NOT_ACTIVE = "Transaction is not active, consider adding @Transactional to your method to automatically activate one."; + + private final TransactionManager transactionManager; + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; + private final SessionFactory sessionFactory; + private final JTAStatelessSessionOpener jtaSessionOpener; + private final String unitName; + private final String sessionKey; + private final Instance requestScopedSessions; + + public TransactionScopedStatelessSession(TransactionManager transactionManager, + TransactionSynchronizationRegistry transactionSynchronizationRegistry, + SessionFactory sessionFactory, + String unitName, + Instance requestScopedSessions) { + this.transactionManager = transactionManager; + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + this.sessionFactory = sessionFactory; + this.jtaSessionOpener = JTAStatelessSessionOpener.create(sessionFactory); + this.unitName = unitName; + this.sessionKey = this.getClass().getSimpleName() + "-" + unitName; + this.requestScopedSessions = requestScopedSessions; + } + + SessionResult acquireSession() { + // TODO: this was copied from TransactionScopedSession, but does it need to be the same??? + if (isInTransaction()) { + StatelessSession session = (StatelessSession) transactionSynchronizationRegistry.getResource(sessionKey); + if (session != null) { + return new SessionResult(session, false, true); + } + StatelessSession newSession = jtaSessionOpener.openSession(); + // The session has automatically joined the JTA transaction when it was constructed. + transactionSynchronizationRegistry.putResource(sessionKey, newSession); + return new SessionResult(newSession, false, true); + } else if (Arc.container().requestContext().isActive()) { + RequestScopedStatelessSessionHolder requestScopedSessions = this.requestScopedSessions.get(); + return new SessionResult(requestScopedSessions.getOrCreateSession(unitName, sessionFactory), + false, false); + } else { + throw new ContextNotActiveException( + "Cannot use the StatelessSession because neither a transaction nor a CDI request context is active." + + " Consider adding @Transactional to your method to automatically activate a transaction," + + " or @ActivateRequestContext if you have valid reasons not to use transactions."); + } + } + + private void checkBlocking() { + if (!BlockingOperationControl.isBlockingAllowed()) { + throw new BlockingOperationNotAllowedException( + "You have attempted to perform a blocking operation on a IO thread. This is not allowed, as blocking the IO thread will cause major performance issues with your application. If you want to perform blocking StatelessSession operations make sure you are doing it from a worker thread."); + } + } + + private boolean isInTransaction() { + try { + switch (transactionManager.getStatus()) { + case Status.STATUS_ACTIVE: + case Status.STATUS_COMMITTING: + case Status.STATUS_MARKED_ROLLBACK: + case Status.STATUS_PREPARED: + case Status.STATUS_PREPARING: + return true; + default: + return false; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void refresh(Object entity) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + if (!emr.allowModification) { + throw new TransactionRequiredException(TRANSACTION_IS_NOT_ACTIVE); + } + emr.statelessSession.refresh(entity); + } + } + + @Deprecated + @Override + public Query createQuery(String qlString) { + checkBlocking(); + //TODO: this needs some thought for how it works outside a tx + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createQuery(qlString); + } + } + + @Override + public Query createQuery(CriteriaQuery criteriaQuery) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createQuery(criteriaQuery); + } + } + + @Deprecated + @Override + public Query createQuery(CriteriaUpdate updateQuery) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createQuery(updateQuery); + } + } + + @Deprecated + @Override + public Query createQuery(CriteriaDelete deleteQuery) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createQuery(deleteQuery); + } + } + + @Override + public Query createQuery(String qlString, Class resultClass) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createQuery(qlString, resultClass); + } + } + + @Deprecated + @Override + public Query createNamedQuery(String name) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNamedQuery(name); + } + } + + @Override + public Query createNamedQuery(String name, Class resultClass) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNamedQuery(name, resultClass); + } + } + + @Deprecated + @Override + public NativeQuery createNativeQuery(String sqlString) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNativeQuery(sqlString); + } + } + + @Deprecated + @Override + public NativeQuery createNativeQuery(String sqlString, Class resultClass) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNativeQuery(sqlString, resultClass); + } + } + + @Deprecated + @Override + public NativeQuery createNativeQuery(String sqlString, String resultSetMapping) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNativeQuery(sqlString, resultSetMapping); + } + } + + @Override + public ProcedureCall createNamedStoredProcedureQuery(String name) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNamedStoredProcedureQuery(name); + } + } + + @Override + public ProcedureCall createStoredProcedureQuery(String procedureName) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createStoredProcedureQuery(procedureName); + } + } + + @Override + public ProcedureCall createStoredProcedureQuery(String procedureName, + @SuppressWarnings("rawtypes") Class... resultClasses) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createStoredProcedureQuery(procedureName, resultClasses); + } + } + + @Override + public ProcedureCall createStoredProcedureQuery(String procedureName, String... resultSetMappings) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createStoredProcedureQuery(procedureName, resultSetMappings); + } + } + + @Override + public void joinTransaction() { + try (SessionResult emr = acquireSession()) { + emr.statelessSession.joinTransaction(); + } + } + + @Override + public boolean isJoinedToTransaction() { + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.isJoinedToTransaction(); + } + } + + @Override + public void close() { + throw new IllegalStateException("Not supported for transaction scoped entity managers"); + } + + @Override + public Object insert(Object o) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.insert(o); + } + } + + @Override + public Object insert(String s, Object o) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.insert(s, o); + } + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public Transaction getTransaction() { + throw new IllegalStateException("Not supported for JTA entity managers"); + } + + @Override + public HibernateCriteriaBuilder getCriteriaBuilder() { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getCriteriaBuilder(); + } + } + + @Deprecated + @Override + public void update(Object object) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.update(object); + } + } + + @Deprecated + @Override + public void update(String entityName, Object object) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.update(entityName, object); + } + } + + @Deprecated + @Override + public void delete(Object object) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.delete(object); + } + } + + @Deprecated + @Override + public void delete(String entityName, Object object) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.delete(entityName, object); + } + } + + @Deprecated + @Override + public void refresh(String entityName, Object object) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.refresh(entityName, object); + } + } + + @Override + public void refresh(Object object, LockMode lockMode) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.refresh(object, lockMode); + } + } + + @Override + public void refresh(String s, Object o, LockMode lockMode) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.refresh(s, o, lockMode); + } + } + + @Override + public void fetch(Object o) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.refresh(o); + } + } + + @Override + public T get(Class entityType, Object id) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.get(entityType, id); + } + } + + @Override + public T get(Class entityType, Object id, LockMode lockMode) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.get(entityType, id, lockMode); + } + } + + @Override + public Object get(String entityName, Object id) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.get(entityName, id); + } + } + + @Override + public Object get(String entityName, Object id, LockMode lockMode) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.get(entityName, id, lockMode); + } + } + + @Override + public String getTenantIdentifier() { + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getTenantIdentifier(); + } + } + + @Override + public boolean isConnected() { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.isConnected(); + } + } + + @Override + public Transaction beginTransaction() { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.beginTransaction(); + } + } + + @Deprecated + @Override + public Query getNamedQuery(String queryName) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getNamedQuery(queryName); + } + } + + @Override + public ProcedureCall getNamedProcedureCall(String name) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getNamedProcedureCall(name); + } + } + + @Override + public ProcedureCall createStoredProcedureCall(String procedureName) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createStoredProcedureCall(procedureName); + } + } + + @Override + public ProcedureCall createStoredProcedureCall(String procedureName, Class... resultClasses) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createStoredProcedureCall(procedureName, resultClasses); + } + } + + @Override + public ProcedureCall createStoredProcedureCall(String procedureName, String... resultSetMappings) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createStoredProcedureCall(procedureName, resultSetMappings); + } + } + + @Override + public Integer getJdbcBatchSize() { + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getJdbcBatchSize(); + } + } + + @Override + public void setJdbcBatchSize(Integer jdbcBatchSize) { + try (SessionResult emr = acquireSession()) { + emr.statelessSession.setJdbcBatchSize(jdbcBatchSize); + } + } + + @Override + public void doWork(Work work) throws HibernateException { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + emr.statelessSession.doWork(work); + } + } + + @Override + public T doReturningWork(ReturningWork work) throws HibernateException { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.doReturningWork(work); + } + } + + @Deprecated + @Override + public NativeQuery getNamedNativeQuery(String name) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getNamedNativeQuery(name); + } + } + + @Override + public NativeQuery createNativeQuery(String sqlString, Class resultClass, String tableAlias) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNativeQuery(sqlString, resultClass, tableAlias); + } + } + + @Override + public NativeQuery createNativeQuery(String sqlString, String resultSetMappingName, Class resultClass) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNativeQuery(sqlString, resultSetMappingName, resultClass); + } + } + + @Override + public SelectionQuery createSelectionQuery(String hqlString) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createSelectionQuery(hqlString); + } + } + + @Override + public SelectionQuery createSelectionQuery(String hqlString, Class resultType) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createSelectionQuery(hqlString, resultType); + } + } + + @Override + public SelectionQuery createSelectionQuery(CriteriaQuery criteria) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createSelectionQuery(criteria); + } + } + + @Override + public MutationQuery createMutationQuery(String hqlString) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createMutationQuery(hqlString); + } + } + + @Override + public MutationQuery createMutationQuery(CriteriaUpdate updateQuery) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createMutationQuery(updateQuery); + } + } + + @Override + public MutationQuery createMutationQuery(CriteriaDelete deleteQuery) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createMutationQuery(deleteQuery); + } + } + + @Override + public MutationQuery createMutationQuery(JpaCriteriaInsertSelect insertSelect) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createMutationQuery(insertSelect); + } + } + + @Override + public MutationQuery createNativeMutationQuery(String sqlString) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNativeMutationQuery(sqlString); + } + } + + @Override + public SelectionQuery createNamedSelectionQuery(String name) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNamedSelectionQuery(name); + } + } + + @Override + public SelectionQuery createNamedSelectionQuery(String name, Class resultType) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNamedSelectionQuery(name, resultType); + } + } + + @Override + public MutationQuery createNamedMutationQuery(String name) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.createNamedMutationQuery(name); + } + } + + @Deprecated + @Override + public NativeQuery getNamedNativeQuery(String name, String resultSetMapping) { + checkBlocking(); + try (SessionResult emr = acquireSession()) { + return emr.statelessSession.getNamedNativeQuery(name, resultSetMapping); + } + } + + static class SessionResult implements AutoCloseable { + + final StatelessSession statelessSession; + final boolean closeOnEnd; + final boolean allowModification; + + SessionResult(StatelessSession statelessSession, boolean closeOnEnd, boolean allowModification) { + this.statelessSession = statelessSession; + this.closeOnEnd = closeOnEnd; + this.allowModification = allowModification; + } + + @Override + public void close() { + if (closeOnEnd) { + statelessSession.close(); + } + } + } +}