diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java index f29b2c479f5..17ebebade89 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java @@ -37,7 +37,7 @@ import org.springframework.util.Assert; */ public class EntityManagerHolder extends ResourceHolderSupport { - private final @Nullable EntityManager entityManager; + protected @Nullable EntityManager entityManager; private boolean transactionActive; @@ -78,4 +78,8 @@ public class EntityManagerHolder extends ResourceHolderSupport { this.savepointManager = null; } + protected void closeAll() { + EntityManagerFactoryUtils.closeEntityManager(this.entityManager); + } + } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java index a452992632c..d02950e98bd 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java @@ -640,11 +640,8 @@ public class JpaTransactionManager extends AbstractPlatformTransactionManager // Remove the entity manager holder from the thread. if (txObject.isNewEntityManagerHolder()) { - EntityManager em = txObject.getEntityManagerHolder().getEntityManager(); - if (logger.isDebugEnabled()) { - logger.debug("Closing JPA EntityManager [" + em + "] after transaction"); - } - EntityManagerFactoryUtils.closeEntityManager(em); + logger.debug("Closing JPA EntityManager after transaction"); + txObject.getEntityManagerHolder().closeAll(); } else { logger.debug("Not closing pre-bound JPA EntityManager after transaction"); diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateTransactionManager.java index 8ad680da3ef..aa1568ab410 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/HibernateTransactionManager.java @@ -17,7 +17,6 @@ package org.springframework.orm.jpa.hibernate; import java.sql.Connection; -import java.util.Map; import java.util.function.Consumer; import javax.sql.DataSource; @@ -29,12 +28,8 @@ import org.hibernate.Interceptor; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; -import org.hibernate.cfg.Environment; -import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.resource.transaction.spi.TransactionStatus; -import org.hibernate.service.UnknownServiceException; import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; @@ -372,7 +367,7 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana // Check for SessionFactory's DataSource. if (this.autodetectDataSource && getDataSource() == null) { - DataSource sfds = determineDataSource(); + DataSource sfds = SpringSessionContext.determineDataSource(obtainSessionFactory()); if (sfds != null) { // Use the SessionFactory's DataSource for exposing transactions to JDBC code. if (logger.isDebugEnabled()) { @@ -384,36 +379,6 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana } } - /** - * Determine the DataSource of the given SessionFactory. - * @return the DataSource, or {@code null} if none found - * @see ConnectionProvider - */ - protected @Nullable DataSource determineDataSource() { - SessionFactory sessionFactory = obtainSessionFactory(); - Map props = sessionFactory.getProperties(); - if (props != null) { - Object dataSourceValue = props.get(Environment.JAKARTA_NON_JTA_DATASOURCE); - if (dataSourceValue instanceof DataSource dataSourceToUse) { - return dataSourceToUse; - } - } - if (sessionFactory instanceof SessionFactoryImplementor sfi) { - try { - ConnectionProvider cp = sfi.getServiceRegistry().getService(ConnectionProvider.class); - if (cp != null) { - return cp.unwrap(DataSource.class); - } - } - catch (UnknownServiceException ex) { - if (logger.isDebugEnabled()) { - logger.debug("No ConnectionProvider found - cannot determine DataSource for SessionFactory: " + ex); - } - } - } - return null; - } - @Override public Object getResourceFactory() { @@ -735,7 +700,7 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana if (logger.isDebugEnabled()) { logger.debug("Closing Hibernate Session [" + session + "] after transaction"); } - EntityManagerFactoryUtils.closeEntityManager(session); + txObject.getSessionHolder().closeAll(); } else { if (logger.isDebugEnabled()) { diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/LocalSessionFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/LocalSessionFactoryBean.java index ed939e66518..722d14ce375 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/LocalSessionFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/LocalSessionFactoryBean.java @@ -23,7 +23,9 @@ import java.util.Properties; import javax.sql.DataSource; import org.hibernate.Interceptor; +import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; @@ -41,6 +43,7 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.SmartFactoryBean; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ResourceLoaderAware; @@ -77,7 +80,7 @@ import org.springframework.core.type.filter.TypeFilter; * @see org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean */ public class LocalSessionFactoryBean extends HibernateExceptionTranslator - implements FactoryBean, ResourceLoaderAware, BeanFactoryAware, + implements SmartFactoryBean, ResourceLoaderAware, BeanFactoryAware, InitializingBean, SmartInitializingSingleton, DisposableBean { private @Nullable DataSource dataSource; @@ -134,6 +137,10 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator private @Nullable SessionFactory sessionFactory; + private @Nullable Session sharedSession; + + private @Nullable StatelessSession sharedStatelessSession; + /** * Set the DataSource to be used by the SessionFactory. @@ -565,6 +572,8 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator // Build SessionFactory instance. this.configuration = sfb; this.sessionFactory = buildSessionFactory(sfb); + this.sharedSession = SharedSessionCreator.createSharedSession(this.sessionFactory); + this.sharedStatelessSession = SharedSessionCreator.createSharedStatelessSession(this.sessionFactory); } @Override @@ -614,9 +623,24 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator return (this.sessionFactory != null ? this.sessionFactory.getClass() : SessionFactory.class); } + /** + * Return either the singleton SessionFactory or a shared (Stateless)Session proxy. + */ + @Override + public @Nullable S getObject(Class type) throws Exception { + if (Session.class.isAssignableFrom(type)) { + return type.cast(this.sharedSession); + } + if (StatelessSession.class.isAssignableFrom(type)) { + return type.cast(this.sharedStatelessSession); + } + return SmartFactoryBean.super.getObject(type); + } + @Override - public boolean isSingleton() { - return true; + public boolean supportsType(Class type) { + return (type == Session.class || type == StatelessSession.class || + SmartFactoryBean.super.supportsType(type)); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java index 405ef8e49a2..f54ac43614a 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SessionHolder.java @@ -18,10 +18,12 @@ package org.springframework.orm.jpa.hibernate; import org.hibernate.FlushMode; import org.hibernate.Session; +import org.hibernate.StatelessSession; import org.hibernate.Transaction; import org.jspecify.annotations.Nullable; import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.util.Assert; /** * Resource holder wrapping a Hibernate {@link Session} (plus an optional {@link Transaction}). @@ -37,6 +39,8 @@ import org.springframework.orm.jpa.EntityManagerHolder; */ class SessionHolder extends EntityManagerHolder { + private @Nullable StatelessSession statelessSession; + private @Nullable Transaction transaction; private @Nullable FlushMode previousFlushMode; @@ -46,11 +50,37 @@ class SessionHolder extends EntityManagerHolder { super(session); } + public SessionHolder(StatelessSession session) { + super(null); + this.statelessSession = session; + } + + + public void setSession(Session session) { + this.entityManager = session; + } public Session getSession() { return (Session) getEntityManager(); } + public boolean hasSession() { + return (this.entityManager != null); + } + + public void setStatelessSession(StatelessSession statelessSession) { + this.statelessSession = statelessSession; + } + + public StatelessSession getStatelessSession() { + Assert.state(this.statelessSession != null, "No StatelessSession available"); + return this.statelessSession; + } + + public boolean hasStatelessSession() { + return (this.statelessSession != null); + } + public void setTransaction(@Nullable Transaction transaction) { this.transaction = transaction; setTransactionActive(transaction != null); @@ -76,4 +106,12 @@ class SessionHolder extends EntityManagerHolder { this.previousFlushMode = null; } + @Override + protected void closeAll() { + super.closeAll(); + if (this.statelessSession != null && this.statelessSession.isOpen()) { + this.statelessSession.close(); + } + } + } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SharedSessionCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SharedSessionCreator.java new file mode 100644 index 00000000000..c9d7e7e124e --- /dev/null +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SharedSessionCreator.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.orm.jpa.hibernate; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.function.Supplier; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; +import org.jspecify.annotations.Nullable; + +/** + * Delegate for creating shareable {@link Session}/{@link StatelessSession} + * references for a given {@link SessionFactory}. + * + *

Typically used next to {@link LocalSessionFactoryBuilder}. Note that + * {@link LocalSessionFactoryBean} exposes shared {@link Session} as well + * as {@link StatelessSession} references for dependency injection already, + * avoiding the need to define separate beans for the shared sessions. + * + * @author Juergen Hoeller + * @since 7.0 + * @see LocalSessionFactoryBuilder + * @see LocalSessionFactoryBean + * @see org.springframework.orm.jpa.SharedEntityManagerCreator + */ +public abstract class SharedSessionCreator { + + /** + * Create a shared {@link Session} proxy for the given {@link SessionFactory}. + *

The returned instance behaves like {@link SessionFactory#getCurrentSession()} + * but without the manual get call, automatically delegating every {@link Session} + * method invocation to the current thread-bound transactional session instance. + * Designed to work with {@link HibernateTransactionManager} as well as JTA. + *

Alternatively, use {@link SessionFactory#getCurrentSession()} directly. + * @param sessionFactory the SessionFactory to build the Session proxy for + * @see SessionFactory#getCurrentSession() + */ + public static Session createSharedSession(SessionFactory sessionFactory) { + return (Session) Proxy.newProxyInstance(SharedSessionCreator.class.getClassLoader(), + new Class[] {Session.class}, + new SharedSessionInvocationHandler(sessionFactory, sessionFactory::getCurrentSession)); + } + + /** + * Create a shared {@link StatelessSession} proxy for the given {@link SessionFactory}. + *

The returned instance automatically delegates every {@link StatelessSession} + * method invocation to the current thread-bound transactional session instance. + * On the first invocation within a new transaction, a {@link StatelessSession} + * will be opened for the current transactional JDBC Connection. + *

Works with {@link HibernateTransactionManager} (side by side with a + * thread-bound regular Session that drives the transaction) as well as + * {@link org.springframework.jdbc.support.JdbcTransactionManager} or + * {@link org.springframework.transaction.jta.JtaTransactionManager} + * (with a plain StatelessSession on top of a transactional JDBC Connection). + *

Alternatively, call {@link SpringSessionContext#currentStatelessSession} + * for every operation, avoiding the need for a proxy. + * @param sessionFactory the SessionFactory to build the StatelessSession proxy for + * @see SpringSessionContext#currentStatelessSession(SessionFactory) + */ + public static StatelessSession createSharedStatelessSession(SessionFactory sessionFactory) { + return (StatelessSession) Proxy.newProxyInstance(SharedSessionCreator.class.getClassLoader(), + new Class[] {StatelessSession.class}, + new SharedSessionInvocationHandler(sessionFactory, + () -> SpringSessionContext.currentStatelessSession(sessionFactory))); + } + + + private static class SharedSessionInvocationHandler implements InvocationHandler { + + private final SessionFactory sessionFactory; + + private final Supplier currentSessionSupplier; + + public SharedSessionInvocationHandler(SessionFactory sessionFactory, Supplier currentSessionSupplier) { + this.sessionFactory = sessionFactory; + this.currentSessionSupplier = currentSessionSupplier; + } + + @Override + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "equals" -> { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } + case "hashCode" -> { + // Use hashCode of EntityManager proxy. + return hashCode(); + } + case "toString" -> { + // Deliver toString without touching a target EntityManager. + return "Shared Session proxy for target factory [" + this.sessionFactory + "]"; + } + case "getSessionFactory", "getEntityManagerFactory" -> { + // JPA 2.0: return EntityManagerFactory without creating an EntityManager. + return this.sessionFactory; + } + case "getCriteriaBuilder", "getMetamodel" -> { + // JPA 2.0: return EntityManagerFactory's CriteriaBuilder/Metamodel (avoid creation of EntityManager) + try { + return SessionFactory.class.getMethod(method.getName()).invoke(this.sessionFactory); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + case "unwrap" -> { + // JPA 2.0: handle unwrap method - could be a proxy match. + Class targetClass = (Class) args[0]; + if (targetClass != null && targetClass.isInstance(proxy)) { + return proxy; + } + } + case "isOpen" -> { + // Handle isOpen method: always return true. + return true; + } + case "close" -> { + // Handle close method: suppress, not valid. + return null; + } + case "getTransaction" -> { + throw new IllegalStateException( + "Not allowed to create transaction on shared EntityManager - " + + "use Spring transactions or EJB CMT instead"); + } + } + + Object target = this.currentSessionSupplier.get(); + try { + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionContext.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionContext.java index 7d63ae33925..10e238a897a 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionContext.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionContext.java @@ -16,6 +16,11 @@ package org.springframework.orm.jpa.hibernate; +import java.sql.Connection; +import java.util.Map; + +import javax.sql.DataSource; + import jakarta.transaction.Status; import jakarta.transaction.SystemException; import jakarta.transaction.TransactionManager; @@ -23,11 +28,18 @@ import org.apache.commons.logging.LogFactory; import org.hibernate.FlushMode; import org.hibernate.HibernateException; import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; +import org.hibernate.cfg.Environment; import org.hibernate.context.spi.CurrentSessionContext; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; +import org.hibernate.service.UnknownServiceException; import org.jspecify.annotations.Nullable; +import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.orm.jpa.EntityManagerHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -78,27 +90,31 @@ public class SpringSessionContext implements CurrentSessionContext { @Override public Session currentSession() throws HibernateException { Object value = TransactionSynchronizationManager.getResource(this.sessionFactory); + SessionHolder holder = null; if (value instanceof Session session) { return session; } else if (value instanceof SessionHolder sessionHolder) { // HibernateTransactionManager - Session session = sessionHolder.getSession(); - if (!sessionHolder.isSynchronizedWithTransaction() && - TransactionSynchronizationManager.isSynchronizationActive()) { - TransactionSynchronizationManager.registerSynchronization( - new SpringSessionSynchronization(sessionHolder, this.sessionFactory, false)); - sessionHolder.setSynchronizedWithTransaction(true); - // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session - // with FlushMode.MANUAL, which needs to allow flushing within the transaction. - FlushMode flushMode = session.getHibernateFlushMode(); - if (flushMode.equals(FlushMode.MANUAL) && - !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { - session.setHibernateFlushMode(FlushMode.AUTO); - sessionHolder.setPreviousFlushMode(flushMode); + if (sessionHolder.hasSession()) { + Session session = sessionHolder.getSession(); + if (!sessionHolder.isSynchronizedWithTransaction() && + TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization( + new SpringSessionSynchronization(sessionHolder, this.sessionFactory, false)); + sessionHolder.setSynchronizedWithTransaction(true); + // Switch to FlushMode.AUTO, as we have to assume a thread-bound Session + // with FlushMode.MANUAL, which needs to allow flushing within the transaction. + FlushMode flushMode = session.getHibernateFlushMode(); + if (flushMode.equals(FlushMode.MANUAL) && + !TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + session.setHibernateFlushMode(FlushMode.AUTO); + sessionHolder.setPreviousFlushMode(flushMode); + } } + return session; } - return session; + holder = sessionHolder; } else if (value instanceof EntityManagerHolder entityManagerHolder) { // JpaTransactionManager @@ -122,15 +138,25 @@ public class SpringSessionContext implements CurrentSessionContext { } if (TransactionSynchronizationManager.isSynchronizationActive()) { - Session session = this.sessionFactory.openSession(); + Session session; + DataSource dataSource = determineDataSource(this.sessionFactory); + if (dataSource != null) { + session = this.sessionFactory.withOptions() + .connection(DataSourceUtils.getConnection(dataSource)) + .openSession(); + } + else { + session = this.sessionFactory.openSession(); + } if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { session.setHibernateFlushMode(FlushMode.MANUAL); } - SessionHolder sessionHolder = new SessionHolder(session); - TransactionSynchronizationManager.registerSynchronization( - new SpringSessionSynchronization(sessionHolder, this.sessionFactory, true)); - TransactionSynchronizationManager.bindResource(this.sessionFactory, sessionHolder); - sessionHolder.setSynchronizedWithTransaction(true); + if (holder != null) { + holder.setSession(session); + } + else { + bindSessionHolder(this.sessionFactory, new SessionHolder(session)); + } return session; } else { @@ -138,4 +164,81 @@ public class SpringSessionContext implements CurrentSessionContext { } } + + /** + * Obtain a {@link StatelessSession} for the current transaction. + * @param sessionFactory the target SessionFactory + * @return the current StatelessSession + */ + public static StatelessSession currentStatelessSession(SessionFactory sessionFactory) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + throw new HibernateException("Could not obtain transaction-synchronized Session for current thread"); + } + Object value = TransactionSynchronizationManager.getResource(sessionFactory); + if (value instanceof StatelessSession statelessSession) { + return statelessSession; + } + SessionHolder holder = null; + if (value instanceof SessionHolder sessionHolder) { + if (sessionHolder.hasStatelessSession()) { + return sessionHolder.getStatelessSession(); + } + holder = sessionHolder; + } + StatelessSession session = sessionFactory.openStatelessSession(determineConnection(sessionFactory, holder)); + if (holder != null) { + holder.setStatelessSession(session); + } + else { + bindSessionHolder(sessionFactory, new SessionHolder(session)); + } + return session; + } + + private static void bindSessionHolder(SessionFactory sessionFactory, SessionHolder holder) { + TransactionSynchronizationManager.registerSynchronization( + new SpringSessionSynchronization(holder, sessionFactory, true)); + TransactionSynchronizationManager.bindResource(sessionFactory, holder); + holder.setSynchronizedWithTransaction(true); + } + + private static Connection determineConnection(SessionFactory sessionFactory, @Nullable SessionHolder holder) { + if (holder != null && holder.getSession() instanceof SessionImplementor session) { + return session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection(); + } + DataSource dataSource = determineDataSource(sessionFactory); + if (dataSource != null) { + return DataSourceUtils.getConnection(dataSource); + } + throw new IllegalStateException( + "Cannot determine JDBC DataSource for Hibernate SessionFactory: " + sessionFactory); + } + + /** + * Determine the DataSource of the given SessionFactory. + * @return the DataSource, or {@code null} if none found + * @see ConnectionProvider + */ + static @Nullable DataSource determineDataSource(SessionFactory sessionFactory) { + Map props = sessionFactory.getProperties(); + if (props != null) { + Object dataSourceValue = props.get(Environment.JAKARTA_NON_JTA_DATASOURCE); + if (dataSourceValue instanceof DataSource dataSourceToUse) { + return dataSourceToUse; + } + } + if (sessionFactory instanceof SessionFactoryImplementor sfi) { + try { + ConnectionProvider cp = sfi.getServiceRegistry().getService(ConnectionProvider.class); + if (cp != null) { + return cp.unwrap(DataSource.class); + } + } + catch (UnknownServiceException ex) { + // Ignore - cannot determine + } + } + return null; + } + } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionSynchronization.java b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionSynchronization.java index 61985b88256..bebd714c9c5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionSynchronization.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/hibernate/SpringSessionSynchronization.java @@ -25,7 +25,6 @@ import org.springframework.core.Ordered; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -44,7 +43,7 @@ class SpringSessionSynchronization implements TransactionSynchronization, Ordere * to execute Session cleanup before JDBC Connection cleanup, if any. * @see DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER */ - private static final int SESSION_SYNCHRONIZATION_ORDER = + static final int SESSION_SYNCHRONIZATION_ORDER = DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 100; private final SessionHolder sessionHolder; @@ -56,10 +55,6 @@ class SpringSessionSynchronization implements TransactionSynchronization, Ordere private boolean holderActive = true; - public SpringSessionSynchronization(SessionHolder sessionHolder, SessionFactory sessionFactory) { - this(sessionHolder, sessionFactory, false); - } - public SpringSessionSynchronization(SessionHolder sessionHolder, SessionFactory sessionFactory, boolean newSession) { this.sessionHolder = sessionHolder; this.sessionFactory = sessionFactory; @@ -162,7 +157,7 @@ class SpringSessionSynchronization implements TransactionSynchronization, Ordere this.sessionHolder.setSynchronizedWithTransaction(false); // Call close() at this point if it's a new Session... if (this.newSession) { - EntityManagerFactoryUtils.closeEntityManager(this.sessionHolder.getSession()); + this.sessionHolder.closeAll(); } } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java index 94d21bd5529..6e45ac277d6 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java @@ -18,16 +18,22 @@ package org.springframework.orm.jpa.hibernate; import java.util.List; +import javax.sql.DataSource; + import org.hibernate.FlushMode; +import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.hibernate.StatelessSession; import org.hibernate.query.Query; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.orm.jpa.AbstractContainerEntityManagerFactoryIntegrationTests; import org.springframework.orm.jpa.EntityManagerFactoryInfo; import org.springframework.orm.jpa.domain.Person; +import org.springframework.transaction.support.TransactionTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -42,6 +48,15 @@ class HibernateNativeEntityManagerFactoryIntegrationTests extends AbstractContai @Autowired private SessionFactory sessionFactory; + @Autowired + private Session sharedSession; + + @Autowired + private StatelessSession statelessSession; + + @Autowired + private DataSource dataSource; + @Autowired private ApplicationContext applicationContext; @@ -83,6 +98,58 @@ class HibernateNativeEntityManagerFactoryIntegrationTests extends AbstractContai assertThat(q.getResultList().get(0).postLoaded).isSameAs(applicationContext); } + @Test + public void testSharedSession() { + String firstName = "Tony"; + insertPerson(firstName); + + Query q = sharedSession.createQuery("select p from Person as p", Person.class); + assertThat(q.getResultList()).hasSize(1); + assertThat(q.getResultList().get(0).getFirstName()).isEqualTo(firstName); + assertThat(q.getResultList().get(0).postLoaded).isSameAs(applicationContext); + + endTransaction(); + + DataSourceTransactionManager dstm = new DataSourceTransactionManager(dataSource); + new TransactionTemplate(dstm).execute(status -> { + insertPerson(firstName); + Query q2 = sharedSession.createQuery("select p from Person as p", Person.class); + assertThat(q2.getResultList()).hasSize(1); + assertThat(q2.getResultList().get(0).getFirstName()).isEqualTo(firstName); + assertThat(q2.getResultList().get(0).postLoaded).isSameAs(applicationContext); + Query q3 = statelessSession.createQuery("select p from Person as p", Person.class); + assertThat(q3.getResultList()).hasSize(1); + assertThat(q3.getResultList().get(0).getFirstName()).isEqualTo(firstName); + status.setRollbackOnly(); + return null; + }); + } + + @Test + public void testStatelessSession() { + String firstName = "Tony"; + insertPerson(firstName); + + Query q = statelessSession.createQuery("select p from Person as p", Person.class); + assertThat(q.getResultList()).hasSize(1); + assertThat(q.getResultList().get(0).getFirstName()).isEqualTo(firstName); + + endTransaction(); + + DataSourceTransactionManager dstm = new DataSourceTransactionManager(dataSource); + new TransactionTemplate(dstm).execute(status -> { + insertPerson(firstName); + Query q2 = statelessSession.createQuery("select p from Person as p", Person.class); + assertThat(q2.getResultList()).hasSize(1); + assertThat(q2.getResultList().get(0).getFirstName()).isEqualTo(firstName); + Query q3 = sharedSession.createQuery("select p from Person as p", Person.class); + assertThat(q3.getResultList()).hasSize(1); + assertThat(q3.getResultList().get(0).getFirstName()).isEqualTo(firstName); + status.setRollbackOnly(); + return null; + }); + } + @Test // SPR-16956 public void testReadOnly() { assertThat(sessionFactory.getCurrentSession().getHibernateFlushMode()).isSameAs(FlushMode.AUTO);