mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Add transactional support for StatelessSession (next to regular Session)
Exposes JPA-style shared proxy instances through LocalSessionFactoryBean. Closes gh-7184
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
+2
-37
@@ -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<String, Object> 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()) {
|
||||
|
||||
+27
-3
@@ -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<SessionFactory>, ResourceLoaderAware, BeanFactoryAware,
|
||||
implements SmartFactoryBean<SessionFactory>, 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 boolean isSingleton() {
|
||||
return true;
|
||||
public <S> @Nullable S getObject(Class<S> 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 supportsType(Class<?> type) {
|
||||
return (type == Session.class || type == StatelessSession.class ||
|
||||
SmartFactoryBean.super.supportsType(type));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+158
@@ -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}.
|
||||
*
|
||||
* <p>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}.
|
||||
* <p>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.
|
||||
* <p>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}.
|
||||
* <p>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.
|
||||
* <p>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).
|
||||
* <p>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<Object> currentSessionSupplier;
|
||||
|
||||
public SharedSessionInvocationHandler(SessionFactory sessionFactory, Supplier<Object> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+123
-20
@@ -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<String, Object> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-7
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
@@ -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<Person> 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<Person> 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<Person> 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<Person> 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<Person> 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<Person> 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);
|
||||
|
||||
Reference in New Issue
Block a user