001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.orm.hibernate5;
018
019import java.sql.Connection;
020import java.sql.ResultSet;
021
022import javax.persistence.PersistenceException;
023import javax.sql.DataSource;
024
025import org.hibernate.ConnectionReleaseMode;
026import org.hibernate.FlushMode;
027import org.hibernate.HibernateException;
028import org.hibernate.Interceptor;
029import org.hibernate.Session;
030import org.hibernate.SessionFactory;
031import org.hibernate.Transaction;
032import org.hibernate.engine.spi.SessionImplementor;
033import org.hibernate.resource.transaction.spi.TransactionStatus;
034
035import org.springframework.beans.BeansException;
036import org.springframework.beans.factory.BeanFactory;
037import org.springframework.beans.factory.BeanFactoryAware;
038import org.springframework.beans.factory.InitializingBean;
039import org.springframework.dao.DataAccessException;
040import org.springframework.dao.DataAccessResourceFailureException;
041import org.springframework.jdbc.datasource.ConnectionHolder;
042import org.springframework.jdbc.datasource.DataSourceUtils;
043import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport;
044import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
045import org.springframework.lang.Nullable;
046import org.springframework.transaction.CannotCreateTransactionException;
047import org.springframework.transaction.IllegalTransactionStateException;
048import org.springframework.transaction.InvalidIsolationLevelException;
049import org.springframework.transaction.TransactionDefinition;
050import org.springframework.transaction.TransactionSystemException;
051import org.springframework.transaction.support.AbstractPlatformTransactionManager;
052import org.springframework.transaction.support.DefaultTransactionStatus;
053import org.springframework.transaction.support.ResourceTransactionManager;
054import org.springframework.transaction.support.TransactionSynchronizationManager;
055import org.springframework.util.Assert;
056
057/**
058 * {@link org.springframework.transaction.PlatformTransactionManager}
059 * implementation for a single Hibernate {@link SessionFactory}.
060 * Binds a Hibernate Session from the specified factory to the thread,
061 * potentially allowing for one thread-bound Session per factory.
062 * {@code SessionFactory.getCurrentSession()} is required for Hibernate
063 * access code that needs to support this transaction handling mechanism,
064 * with the SessionFactory being configured with {@link SpringSessionContext}.
065 *
066 * <p>Supports custom isolation levels, and timeouts that get applied as
067 * Hibernate transaction timeouts.
068 *
069 * <p>This transaction manager is appropriate for applications that use a single
070 * Hibernate SessionFactory for transactional data access, but it also supports
071 * direct DataSource access within a transaction (i.e. plain JDBC code working
072 * with the same DataSource). This allows for mixing services which access Hibernate
073 * and services which use plain JDBC (without being aware of Hibernate)!
074 * Application code needs to stick to the same simple Connection lookup pattern as
075 * with {@link org.springframework.jdbc.datasource.DataSourceTransactionManager}
076 * (i.e. {@link org.springframework.jdbc.datasource.DataSourceUtils#getConnection}
077 * or going through a
078 * {@link org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy}).
079 *
080 * <p>Note: To be able to register a DataSource's Connection for plain JDBC code,
081 * this instance needs to be aware of the DataSource ({@link #setDataSource}).
082 * The given DataSource should obviously match the one used by the given SessionFactory.
083 *
084 * <p>JTA (usually through {@link org.springframework.transaction.jta.JtaTransactionManager})
085 * is necessary for accessing multiple transactional resources within the same
086 * transaction. The DataSource that Hibernate uses needs to be JTA-enabled in
087 * such a scenario (see container setup).
088 *
089 * <p>This transaction manager supports nested transactions via JDBC 3.0 Savepoints.
090 * The {@link #setNestedTransactionAllowed} "nestedTransactionAllowed"} flag defaults
091 * to "false", though, as nested transactions will just apply to the JDBC Connection,
092 * not to the Hibernate Session and its cached entity objects and related context.
093 * You can manually set the flag to "true" if you want to use nested transactions
094 * for JDBC access code which participates in Hibernate transactions (provided that
095 * your JDBC driver supports Savepoints). <i>Note that Hibernate itself does not
096 * support nested transactions! Hence, do not expect Hibernate access code to
097 * semantically participate in a nested transaction.</i>
098 *
099 * @author Juergen Hoeller
100 * @since 4.2
101 * @see #setSessionFactory
102 * @see #setDataSource
103 * @see SessionFactory#getCurrentSession()
104 * @see DataSourceUtils#getConnection
105 * @see DataSourceUtils#releaseConnection
106 * @see org.springframework.jdbc.core.JdbcTemplate
107 * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
108 * @see org.springframework.transaction.jta.JtaTransactionManager
109 */
110@SuppressWarnings("serial")
111public class HibernateTransactionManager extends AbstractPlatformTransactionManager
112                implements ResourceTransactionManager, BeanFactoryAware, InitializingBean {
113
114        @Nullable
115        private SessionFactory sessionFactory;
116
117        @Nullable
118        private DataSource dataSource;
119
120        private boolean autodetectDataSource = true;
121
122        private boolean prepareConnection = true;
123
124        private boolean allowResultAccessAfterCompletion = false;
125
126        private boolean hibernateManagedSession = false;
127
128        @Nullable
129        private Object entityInterceptor;
130
131        /**
132         * Just needed for entityInterceptorBeanName.
133         * @see #setEntityInterceptorBeanName
134         */
135        @Nullable
136        private BeanFactory beanFactory;
137
138
139        /**
140         * Create a new HibernateTransactionManager instance.
141         * A SessionFactory has to be set to be able to use it.
142         * @see #setSessionFactory
143         */
144        public HibernateTransactionManager() {
145        }
146
147        /**
148         * Create a new HibernateTransactionManager instance.
149         * @param sessionFactory the SessionFactory to manage transactions for
150         */
151        public HibernateTransactionManager(SessionFactory sessionFactory) {
152                this.sessionFactory = sessionFactory;
153                afterPropertiesSet();
154        }
155
156
157        /**
158         * Set the SessionFactory that this instance should manage transactions for.
159         */
160        public void setSessionFactory(@Nullable SessionFactory sessionFactory) {
161                this.sessionFactory = sessionFactory;
162        }
163
164        /**
165         * Return the SessionFactory that this instance should manage transactions for.
166         */
167        @Nullable
168        public SessionFactory getSessionFactory() {
169                return this.sessionFactory;
170        }
171
172        /**
173         * Obtain the SessionFactory for actual use.
174         * @return the SessionFactory (never {@code null})
175         * @throws IllegalStateException in case of no SessionFactory set
176         * @since 5.0
177         */
178        protected final SessionFactory obtainSessionFactory() {
179                SessionFactory sessionFactory = getSessionFactory();
180                Assert.state(sessionFactory != null, "No SessionFactory set");
181                return sessionFactory;
182        }
183
184        /**
185         * Set the JDBC DataSource that this instance should manage transactions for.
186         * The DataSource should match the one used by the Hibernate SessionFactory:
187         * for example, you could specify the same JNDI DataSource for both.
188         * <p>If the SessionFactory was configured with LocalDataSourceConnectionProvider,
189         * i.e. by Spring's LocalSessionFactoryBean with a specified "dataSource",
190         * the DataSource will be auto-detected: You can still explicitly specify the
191         * DataSource, but you don't need to in this case.
192         * <p>A transactional JDBC Connection for this DataSource will be provided to
193         * application code accessing this DataSource directly via DataSourceUtils
194         * or JdbcTemplate. The Connection will be taken from the Hibernate Session.
195         * <p>The DataSource specified here should be the target DataSource to manage
196         * transactions for, not a TransactionAwareDataSourceProxy. Only data access
197         * code may work with TransactionAwareDataSourceProxy, while the transaction
198         * manager needs to work on the underlying target DataSource. If there's
199         * nevertheless a TransactionAwareDataSourceProxy passed in, it will be
200         * unwrapped to extract its target DataSource.
201         * <p><b>NOTE: For scenarios with many transactions that just read data from
202         * Hibernate's cache (and do not actually access the database), consider using
203         * a {@link org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy}
204         * for the actual target DataSource. Alternatively, consider switching
205         * {@link #setPrepareConnection "prepareConnection"} to {@code false}.</b>
206         * In both cases, this transaction manager will not eagerly acquire a
207         * JDBC Connection for each Hibernate Session anymore (as of Spring 5.1).
208         * @see #setAutodetectDataSource
209         * @see TransactionAwareDataSourceProxy
210         * @see org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy
211         * @see org.springframework.jdbc.core.JdbcTemplate
212         */
213        public void setDataSource(@Nullable DataSource dataSource) {
214                if (dataSource instanceof TransactionAwareDataSourceProxy) {
215                        // If we got a TransactionAwareDataSourceProxy, we need to perform transactions
216                        // for its underlying target DataSource, else data access code won't see
217                        // properly exposed transactions (i.e. transactions for the target DataSource).
218                        this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource();
219                }
220                else {
221                        this.dataSource = dataSource;
222                }
223        }
224
225        /**
226         * Return the JDBC DataSource that this instance manages transactions for.
227         */
228        @Nullable
229        public DataSource getDataSource() {
230                return this.dataSource;
231        }
232
233        /**
234         * Set whether to autodetect a JDBC DataSource used by the Hibernate SessionFactory,
235         * if set via LocalSessionFactoryBean's {@code setDataSource}. Default is "true".
236         * <p>Can be turned off to deliberately ignore an available DataSource, in order
237         * to not expose Hibernate transactions as JDBC transactions for that DataSource.
238         * @see #setDataSource
239         */
240        public void setAutodetectDataSource(boolean autodetectDataSource) {
241                this.autodetectDataSource = autodetectDataSource;
242        }
243
244        /**
245         * Set whether to prepare the underlying JDBC Connection of a transactional
246         * Hibernate Session, that is, whether to apply a transaction-specific
247         * isolation level and/or the transaction's read-only flag to the underlying
248         * JDBC Connection.
249         * <p>Default is "true". If you turn this flag off, the transaction manager
250         * will not support per-transaction isolation levels anymore. It will not
251         * call {@code Connection.setReadOnly(true)} for read-only transactions
252         * anymore either. If this flag is turned off, no cleanup of a JDBC Connection
253         * is required after a transaction, since no Connection settings will get modified.
254         * @see Connection#setTransactionIsolation
255         * @see Connection#setReadOnly
256         */
257        public void setPrepareConnection(boolean prepareConnection) {
258                this.prepareConnection = prepareConnection;
259        }
260
261        /**
262         * Set whether to allow result access after completion, typically via Hibernate's
263         * ScrollableResults mechanism.
264         * <p>Default is "false". Turning this flag on enforces over-commit holdability on the
265         * underlying JDBC Connection (if {@link #prepareConnection "prepareConnection"} is on)
266         * and skips the disconnect-on-completion step.
267         * @see Connection#setHoldability
268         * @see ResultSet#HOLD_CURSORS_OVER_COMMIT
269         * @see #disconnectOnCompletion(Session)
270         */
271        public void setAllowResultAccessAfterCompletion(boolean allowResultAccessAfterCompletion) {
272                this.allowResultAccessAfterCompletion = allowResultAccessAfterCompletion;
273        }
274
275        /**
276         * Set whether to operate on a Hibernate-managed Session instead of a
277         * Spring-managed Session, that is, whether to obtain the Session through
278         * Hibernate's {@link SessionFactory#getCurrentSession()}
279         * instead of {@link SessionFactory#openSession()} (with a Spring
280         * {@link TransactionSynchronizationManager}
281         * check preceding it).
282         * <p>Default is "false", i.e. using a Spring-managed Session: taking the current
283         * thread-bound Session if available (e.g. in an Open-Session-in-View scenario),
284         * creating a new Session for the current transaction otherwise.
285         * <p>Switch this flag to "true" in order to enforce use of a Hibernate-managed Session.
286         * Note that this requires {@link SessionFactory#getCurrentSession()}
287         * to always return a proper Session when called for a Spring-managed transaction;
288         * transaction begin will fail if the {@code getCurrentSession()} call fails.
289         * <p>This mode will typically be used in combination with a custom Hibernate
290         * {@link org.hibernate.context.spi.CurrentSessionContext} implementation that stores
291         * Sessions in a place other than Spring's TransactionSynchronizationManager.
292         * It may also be used in combination with Spring's Open-Session-in-View support
293         * (using Spring's default {@link SpringSessionContext}), in which case it subtly
294         * differs from the Spring-managed Session mode: The pre-bound Session will <i>not</i>
295         * receive a {@code clear()} call (on rollback) or a {@code disconnect()}
296         * call (on transaction completion) in such a scenario; this is rather left up
297         * to a custom CurrentSessionContext implementation (if desired).
298         */
299        public void setHibernateManagedSession(boolean hibernateManagedSession) {
300                this.hibernateManagedSession = hibernateManagedSession;
301        }
302
303        /**
304         * Set the bean name of a Hibernate entity interceptor that allows to inspect
305         * and change property values before writing to and reading from the database.
306         * Will get applied to any new Session created by this transaction manager.
307         * <p>Requires the bean factory to be known, to be able to resolve the bean
308         * name to an interceptor instance on session creation. Typically used for
309         * prototype interceptors, i.e. a new interceptor instance per session.
310         * <p>Can also be used for shared interceptor instances, but it is recommended
311         * to set the interceptor reference directly in such a scenario.
312         * @param entityInterceptorBeanName the name of the entity interceptor in
313         * the bean factory
314         * @see #setBeanFactory
315         * @see #setEntityInterceptor
316         */
317        public void setEntityInterceptorBeanName(String entityInterceptorBeanName) {
318                this.entityInterceptor = entityInterceptorBeanName;
319        }
320
321        /**
322         * Set a Hibernate entity interceptor that allows to inspect and change
323         * property values before writing to and reading from the database.
324         * Will get applied to any new Session created by this transaction manager.
325         * <p>Such an interceptor can either be set at the SessionFactory level,
326         * i.e. on LocalSessionFactoryBean, or at the Session level, i.e. on
327         * HibernateTransactionManager.
328         * @see LocalSessionFactoryBean#setEntityInterceptor
329         */
330        public void setEntityInterceptor(@Nullable Interceptor entityInterceptor) {
331                this.entityInterceptor = entityInterceptor;
332        }
333
334        /**
335         * Return the current Hibernate entity interceptor, or {@code null} if none.
336         * Resolves an entity interceptor bean name via the bean factory,
337         * if necessary.
338         * @throws IllegalStateException if bean name specified but no bean factory set
339         * @throws BeansException if bean name resolution via the bean factory failed
340         * @see #setEntityInterceptor
341         * @see #setEntityInterceptorBeanName
342         * @see #setBeanFactory
343         */
344        @Nullable
345        public Interceptor getEntityInterceptor() throws IllegalStateException, BeansException {
346                if (this.entityInterceptor instanceof Interceptor) {
347                        return (Interceptor) this.entityInterceptor;
348                }
349                else if (this.entityInterceptor instanceof String) {
350                        if (this.beanFactory == null) {
351                                throw new IllegalStateException("Cannot get entity interceptor via bean name if no bean factory set");
352                        }
353                        String beanName = (String) this.entityInterceptor;
354                        return this.beanFactory.getBean(beanName, Interceptor.class);
355                }
356                else {
357                        return null;
358                }
359        }
360
361        /**
362         * The bean factory just needs to be known for resolving entity interceptor
363         * bean names. It does not need to be set for any other mode of operation.
364         * @see #setEntityInterceptorBeanName
365         */
366        @Override
367        public void setBeanFactory(BeanFactory beanFactory) {
368                this.beanFactory = beanFactory;
369        }
370
371        @Override
372        public void afterPropertiesSet() {
373                if (getSessionFactory() == null) {
374                        throw new IllegalArgumentException("Property 'sessionFactory' is required");
375                }
376                if (this.entityInterceptor instanceof String && this.beanFactory == null) {
377                        throw new IllegalArgumentException("Property 'beanFactory' is required for 'entityInterceptorBeanName'");
378                }
379
380                // Check for SessionFactory's DataSource.
381                if (this.autodetectDataSource && getDataSource() == null) {
382                        DataSource sfds = SessionFactoryUtils.getDataSource(getSessionFactory());
383                        if (sfds != null) {
384                                // Use the SessionFactory's DataSource for exposing transactions to JDBC code.
385                                if (logger.isDebugEnabled()) {
386                                        logger.debug("Using DataSource [" + sfds +
387                                                        "] of Hibernate SessionFactory for HibernateTransactionManager");
388                                }
389                                setDataSource(sfds);
390                        }
391                }
392        }
393
394
395        @Override
396        public Object getResourceFactory() {
397                return obtainSessionFactory();
398        }
399
400        @Override
401        protected Object doGetTransaction() {
402                HibernateTransactionObject txObject = new HibernateTransactionObject();
403                txObject.setSavepointAllowed(isNestedTransactionAllowed());
404
405                SessionFactory sessionFactory = obtainSessionFactory();
406                SessionHolder sessionHolder =
407                                (SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
408                if (sessionHolder != null) {
409                        if (logger.isDebugEnabled()) {
410                                logger.debug("Found thread-bound Session [" + sessionHolder.getSession() + "] for Hibernate transaction");
411                        }
412                        txObject.setSessionHolder(sessionHolder);
413                }
414                else if (this.hibernateManagedSession) {
415                        try {
416                                Session session = sessionFactory.getCurrentSession();
417                                if (logger.isDebugEnabled()) {
418                                        logger.debug("Found Hibernate-managed Session [" + session + "] for Spring-managed transaction");
419                                }
420                                txObject.setExistingSession(session);
421                        }
422                        catch (HibernateException ex) {
423                                throw new DataAccessResourceFailureException(
424                                                "Could not obtain Hibernate-managed Session for Spring-managed transaction", ex);
425                        }
426                }
427
428                if (getDataSource() != null) {
429                        ConnectionHolder conHolder = (ConnectionHolder)
430                                        TransactionSynchronizationManager.getResource(getDataSource());
431                        txObject.setConnectionHolder(conHolder);
432                }
433
434                return txObject;
435        }
436
437        @Override
438        protected boolean isExistingTransaction(Object transaction) {
439                HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
440                return (txObject.hasSpringManagedTransaction() ||
441                                (this.hibernateManagedSession && txObject.hasHibernateManagedTransaction()));
442        }
443
444        @Override
445        @SuppressWarnings("deprecation")
446        protected void doBegin(Object transaction, TransactionDefinition definition) {
447                HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
448
449                if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
450                        throw new IllegalTransactionStateException(
451                                        "Pre-bound JDBC Connection found! HibernateTransactionManager does not support " +
452                                        "running within DataSourceTransactionManager if told to manage the DataSource itself. " +
453                                        "It is recommended to use a single HibernateTransactionManager for all transactions " +
454                                        "on a single DataSource, no matter whether Hibernate or JDBC access.");
455                }
456
457                Session session = null;
458
459                try {
460                        if (!txObject.hasSessionHolder() || txObject.getSessionHolder().isSynchronizedWithTransaction()) {
461                                Interceptor entityInterceptor = getEntityInterceptor();
462                                Session newSession = (entityInterceptor != null ?
463                                                obtainSessionFactory().withOptions().interceptor(entityInterceptor).openSession() :
464                                                obtainSessionFactory().openSession());
465                                if (logger.isDebugEnabled()) {
466                                        logger.debug("Opened new Session [" + newSession + "] for Hibernate transaction");
467                                }
468                                txObject.setSession(newSession);
469                        }
470
471                        session = txObject.getSessionHolder().getSession();
472
473                        boolean holdabilityNeeded = this.allowResultAccessAfterCompletion && !txObject.isNewSession();
474                        boolean isolationLevelNeeded = (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT);
475                        if (holdabilityNeeded || isolationLevelNeeded || definition.isReadOnly()) {
476                                if (this.prepareConnection && isSameConnectionForEntireSession(session)) {
477                                        // We're allowed to change the transaction settings of the JDBC Connection.
478                                        if (logger.isDebugEnabled()) {
479                                                logger.debug("Preparing JDBC Connection of Hibernate Session [" + session + "]");
480                                        }
481                                        Connection con = ((SessionImplementor) session).connection();
482                                        Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
483                                        txObject.setPreviousIsolationLevel(previousIsolationLevel);
484                                        txObject.setReadOnly(definition.isReadOnly());
485                                        if (this.allowResultAccessAfterCompletion && !txObject.isNewSession()) {
486                                                int currentHoldability = con.getHoldability();
487                                                if (currentHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) {
488                                                        txObject.setPreviousHoldability(currentHoldability);
489                                                        con.setHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);
490                                                }
491                                        }
492                                }
493                                else {
494                                        // Not allowed to change the transaction settings of the JDBC Connection.
495                                        if (isolationLevelNeeded) {
496                                                // We should set a specific isolation level but are not allowed to...
497                                                throw new InvalidIsolationLevelException(
498                                                                "HibernateTransactionManager is not allowed to support custom isolation levels: " +
499                                                                "make sure that its 'prepareConnection' flag is on (the default) and that the " +
500                                                                "Hibernate connection release mode is set to 'on_close' (the default for JDBC).");
501                                        }
502                                        if (logger.isDebugEnabled()) {
503                                                logger.debug("Not preparing JDBC Connection of Hibernate Session [" + session + "]");
504                                        }
505                                }
506                        }
507
508                        if (definition.isReadOnly() && txObject.isNewSession()) {
509                                // Just set to MANUAL in case of a new Session for this transaction.
510                                session.setFlushMode(FlushMode.MANUAL);
511                                // As of 5.1, we're also setting Hibernate's read-only entity mode by default.
512                                session.setDefaultReadOnly(true);
513                        }
514
515                        if (!definition.isReadOnly() && !txObject.isNewSession()) {
516                                // We need AUTO or COMMIT for a non-read-only transaction.
517                                FlushMode flushMode = SessionFactoryUtils.getFlushMode(session);
518                                if (FlushMode.MANUAL.equals(flushMode)) {
519                                        session.setFlushMode(FlushMode.AUTO);
520                                        txObject.getSessionHolder().setPreviousFlushMode(flushMode);
521                                }
522                        }
523
524                        Transaction hibTx;
525
526                        // Register transaction timeout.
527                        int timeout = determineTimeout(definition);
528                        if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
529                                // Use Hibernate's own transaction timeout mechanism on Hibernate 3.1+
530                                // Applies to all statements, also to inserts, updates and deletes!
531                                hibTx = session.getTransaction();
532                                hibTx.setTimeout(timeout);
533                                hibTx.begin();
534                        }
535                        else {
536                                // Open a plain Hibernate transaction without specified timeout.
537                                hibTx = session.beginTransaction();
538                        }
539
540                        // Add the Hibernate transaction to the session holder.
541                        txObject.getSessionHolder().setTransaction(hibTx);
542
543                        // Register the Hibernate Session's JDBC Connection for the DataSource, if set.
544                        if (getDataSource() != null) {
545                                SessionImplementor sessionImpl = (SessionImplementor) session;
546                                // The following needs to use a lambda expression instead of a method reference
547                                // for compatibility with Hibernate ORM <5.2 where connection() is defined on
548                                // SessionImplementor itself instead of on SharedSessionContractImplementor...
549                                ConnectionHolder conHolder = new ConnectionHolder(() -> sessionImpl.connection());
550                                if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
551                                        conHolder.setTimeoutInSeconds(timeout);
552                                }
553                                if (logger.isDebugEnabled()) {
554                                        logger.debug("Exposing Hibernate transaction as JDBC [" + conHolder.getConnectionHandle() + "]");
555                                }
556                                TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
557                                txObject.setConnectionHolder(conHolder);
558                        }
559
560                        // Bind the session holder to the thread.
561                        if (txObject.isNewSessionHolder()) {
562                                TransactionSynchronizationManager.bindResource(obtainSessionFactory(), txObject.getSessionHolder());
563                        }
564                        txObject.getSessionHolder().setSynchronizedWithTransaction(true);
565                }
566
567                catch (Throwable ex) {
568                        if (txObject.isNewSession()) {
569                                try {
570                                        if (session != null && session.getTransaction().getStatus() == TransactionStatus.ACTIVE) {
571                                                session.getTransaction().rollback();
572                                        }
573                                }
574                                catch (Throwable ex2) {
575                                        logger.debug("Could not rollback Session after failed transaction begin", ex);
576                                }
577                                finally {
578                                        SessionFactoryUtils.closeSession(session);
579                                        txObject.setSessionHolder(null);
580                                }
581                        }
582                        throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex);
583                }
584        }
585
586        @Override
587        protected Object doSuspend(Object transaction) {
588                HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
589                txObject.setSessionHolder(null);
590                SessionHolder sessionHolder =
591                                (SessionHolder) TransactionSynchronizationManager.unbindResource(obtainSessionFactory());
592                txObject.setConnectionHolder(null);
593                ConnectionHolder connectionHolder = null;
594                if (getDataSource() != null) {
595                        connectionHolder = (ConnectionHolder) TransactionSynchronizationManager.unbindResource(getDataSource());
596                }
597                return new SuspendedResourcesHolder(sessionHolder, connectionHolder);
598        }
599
600        @Override
601        protected void doResume(@Nullable Object transaction, Object suspendedResources) {
602                SessionFactory sessionFactory = obtainSessionFactory();
603
604                SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder) suspendedResources;
605                if (TransactionSynchronizationManager.hasResource(sessionFactory)) {
606                        // From non-transactional code running in active transaction synchronization
607                        // -> can be safely removed, will be closed on transaction completion.
608                        TransactionSynchronizationManager.unbindResource(sessionFactory);
609                }
610                TransactionSynchronizationManager.bindResource(sessionFactory, resourcesHolder.getSessionHolder());
611                if (getDataSource() != null && resourcesHolder.getConnectionHolder() != null) {
612                        TransactionSynchronizationManager.bindResource(getDataSource(), resourcesHolder.getConnectionHolder());
613                }
614        }
615
616        @Override
617        protected void doCommit(DefaultTransactionStatus status) {
618                HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
619                Transaction hibTx = txObject.getSessionHolder().getTransaction();
620                Assert.state(hibTx != null, "No Hibernate transaction");
621                if (status.isDebug()) {
622                        logger.debug("Committing Hibernate transaction on Session [" +
623                                        txObject.getSessionHolder().getSession() + "]");
624                }
625
626                try {
627                        hibTx.commit();
628                }
629                catch (org.hibernate.TransactionException ex) {
630                        // assumably from commit call to the underlying JDBC connection
631                        throw new TransactionSystemException("Could not commit Hibernate transaction", ex);
632                }
633                catch (HibernateException ex) {
634                        // assumably failed to flush changes to database
635                        throw convertHibernateAccessException(ex);
636                }
637                catch (PersistenceException ex) {
638                        if (ex.getCause() instanceof HibernateException) {
639                                throw convertHibernateAccessException((HibernateException) ex.getCause());
640                        }
641                        throw ex;
642                }
643        }
644
645        @Override
646        protected void doRollback(DefaultTransactionStatus status) {
647                HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
648                Transaction hibTx = txObject.getSessionHolder().getTransaction();
649                Assert.state(hibTx != null, "No Hibernate transaction");
650                if (status.isDebug()) {
651                        logger.debug("Rolling back Hibernate transaction on Session [" +
652                                        txObject.getSessionHolder().getSession() + "]");
653                }
654
655                try {
656                        hibTx.rollback();
657                }
658                catch (org.hibernate.TransactionException ex) {
659                        throw new TransactionSystemException("Could not roll back Hibernate transaction", ex);
660                }
661                catch (HibernateException ex) {
662                        // Shouldn't really happen, as a rollback doesn't cause a flush.
663                        throw convertHibernateAccessException(ex);
664                }
665                catch (PersistenceException ex) {
666                        if (ex.getCause() instanceof HibernateException) {
667                                throw convertHibernateAccessException((HibernateException) ex.getCause());
668                        }
669                        throw ex;
670                }
671                finally {
672                        if (!txObject.isNewSession() && !this.hibernateManagedSession) {
673                                // Clear all pending inserts/updates/deletes in the Session.
674                                // Necessary for pre-bound Sessions, to avoid inconsistent state.
675                                txObject.getSessionHolder().getSession().clear();
676                        }
677                }
678        }
679
680        @Override
681        protected void doSetRollbackOnly(DefaultTransactionStatus status) {
682                HibernateTransactionObject txObject = (HibernateTransactionObject) status.getTransaction();
683                if (status.isDebug()) {
684                        logger.debug("Setting Hibernate transaction on Session [" +
685                                        txObject.getSessionHolder().getSession() + "] rollback-only");
686                }
687                txObject.setRollbackOnly();
688        }
689
690        @Override
691        @SuppressWarnings("deprecation")
692        protected void doCleanupAfterCompletion(Object transaction) {
693                HibernateTransactionObject txObject = (HibernateTransactionObject) transaction;
694
695                // Remove the session holder from the thread.
696                if (txObject.isNewSessionHolder()) {
697                        TransactionSynchronizationManager.unbindResource(obtainSessionFactory());
698                }
699
700                // Remove the JDBC connection holder from the thread, if exposed.
701                if (getDataSource() != null) {
702                        TransactionSynchronizationManager.unbindResource(getDataSource());
703                }
704
705                Session session = txObject.getSessionHolder().getSession();
706                if (this.prepareConnection && isPhysicallyConnected(session)) {
707                        // We're running with connection release mode "on_close": We're able to reset
708                        // the isolation level and/or read-only flag of the JDBC Connection here.
709                        // Else, we need to rely on the connection pool to perform proper cleanup.
710                        try {
711                                Connection con = ((SessionImplementor) session).connection();
712                                Integer previousHoldability = txObject.getPreviousHoldability();
713                                if (previousHoldability != null) {
714                                        con.setHoldability(previousHoldability);
715                                }
716                                DataSourceUtils.resetConnectionAfterTransaction(
717                                                con, txObject.getPreviousIsolationLevel(), txObject.isReadOnly());
718                        }
719                        catch (HibernateException ex) {
720                                logger.debug("Could not access JDBC Connection of Hibernate Session", ex);
721                        }
722                        catch (Throwable ex) {
723                                logger.debug("Could not reset JDBC Connection after transaction", ex);
724                        }
725                }
726
727                if (txObject.isNewSession()) {
728                        if (logger.isDebugEnabled()) {
729                                logger.debug("Closing Hibernate Session [" + session + "] after transaction");
730                        }
731                        SessionFactoryUtils.closeSession(session);
732                }
733                else {
734                        if (logger.isDebugEnabled()) {
735                                logger.debug("Not closing pre-bound Hibernate Session [" + session + "] after transaction");
736                        }
737                        if (txObject.getSessionHolder().getPreviousFlushMode() != null) {
738                                session.setFlushMode(txObject.getSessionHolder().getPreviousFlushMode());
739                        }
740                        if (!this.allowResultAccessAfterCompletion && !this.hibernateManagedSession) {
741                                disconnectOnCompletion(session);
742                        }
743                }
744                txObject.getSessionHolder().clear();
745        }
746
747        /**
748         * Disconnect a pre-existing Hibernate Session on transaction completion,
749         * returning its database connection but preserving its entity state.
750         * <p>The default implementation simply calls {@link Session#disconnect()}.
751         * Subclasses may override this with a no-op or with fine-tuned disconnection logic.
752         * @param session the Hibernate Session to disconnect
753         * @see Session#disconnect()
754         */
755        protected void disconnectOnCompletion(Session session) {
756                session.disconnect();
757        }
758
759        /**
760         * Return whether the given Hibernate Session will always hold the same
761         * JDBC Connection. This is used to check whether the transaction manager
762         * can safely prepare and clean up the JDBC Connection used for a transaction.
763         * <p>The default implementation checks the Session's connection release mode
764         * to be "on_close".
765         * @param session the Hibernate Session to check
766         * @see ConnectionReleaseMode#ON_CLOSE
767         */
768        @SuppressWarnings("deprecation")
769        protected boolean isSameConnectionForEntireSession(Session session) {
770                if (!(session instanceof SessionImplementor)) {
771                        // The best we can do is to assume we're safe.
772                        return true;
773                }
774                ConnectionReleaseMode releaseMode =
775                                ((SessionImplementor) session).getJdbcCoordinator().getConnectionReleaseMode();
776                return ConnectionReleaseMode.ON_CLOSE.equals(releaseMode);
777        }
778
779        /**
780         * Determine whether the given Session is (still) physically connected
781         * to the database, that is, holds an active JDBC Connection internally.
782         * @param session the Hibernate Session to check
783         * @see #isSameConnectionForEntireSession(Session)
784         */
785        protected boolean isPhysicallyConnected(Session session) {
786                if (!(session instanceof SessionImplementor)) {
787                        // The best we can do is to check whether we're logically connected.
788                        return session.isConnected();
789                }
790                return ((SessionImplementor) session).getJdbcCoordinator().getLogicalConnection().isPhysicallyConnected();
791        }
792
793
794        /**
795         * Convert the given HibernateException to an appropriate exception
796         * from the {@code org.springframework.dao} hierarchy.
797         * <p>Will automatically apply a specified SQLExceptionTranslator to a
798         * Hibernate JDBCException, else rely on Hibernate's default translation.
799         * @param ex the HibernateException that occurred
800         * @return a corresponding DataAccessException
801         * @see SessionFactoryUtils#convertHibernateAccessException
802         */
803        protected DataAccessException convertHibernateAccessException(HibernateException ex) {
804                return SessionFactoryUtils.convertHibernateAccessException(ex);
805        }
806
807
808        /**
809         * Hibernate transaction object, representing a SessionHolder.
810         * Used as transaction object by HibernateTransactionManager.
811         */
812        private class HibernateTransactionObject extends JdbcTransactionObjectSupport {
813
814                @Nullable
815                private SessionHolder sessionHolder;
816
817                private boolean newSessionHolder;
818
819                private boolean newSession;
820
821                @Nullable
822                private Integer previousHoldability;
823
824                public void setSession(Session session) {
825                        this.sessionHolder = new SessionHolder(session);
826                        this.newSessionHolder = true;
827                        this.newSession = true;
828                }
829
830                public void setExistingSession(Session session) {
831                        this.sessionHolder = new SessionHolder(session);
832                        this.newSessionHolder = true;
833                        this.newSession = false;
834                }
835
836                public void setSessionHolder(@Nullable SessionHolder sessionHolder) {
837                        this.sessionHolder = sessionHolder;
838                        this.newSessionHolder = false;
839                        this.newSession = false;
840                }
841
842                public SessionHolder getSessionHolder() {
843                        Assert.state(this.sessionHolder != null, "No SessionHolder available");
844                        return this.sessionHolder;
845                }
846
847                public boolean hasSessionHolder() {
848                        return (this.sessionHolder != null);
849                }
850
851                public boolean isNewSessionHolder() {
852                        return this.newSessionHolder;
853                }
854
855                public boolean isNewSession() {
856                        return this.newSession;
857                }
858
859                public void setPreviousHoldability(@Nullable Integer previousHoldability) {
860                        this.previousHoldability = previousHoldability;
861                }
862
863                @Nullable
864                public Integer getPreviousHoldability() {
865                        return this.previousHoldability;
866                }
867
868                public boolean hasSpringManagedTransaction() {
869                        return (this.sessionHolder != null && this.sessionHolder.getTransaction() != null);
870                }
871
872                public boolean hasHibernateManagedTransaction() {
873                        return (this.sessionHolder != null &&
874                                        this.sessionHolder.getSession().getTransaction().getStatus() == TransactionStatus.ACTIVE);
875                }
876
877                public void setRollbackOnly() {
878                        getSessionHolder().setRollbackOnly();
879                        if (hasConnectionHolder()) {
880                                getConnectionHolder().setRollbackOnly();
881                        }
882                }
883
884                @Override
885                public boolean isRollbackOnly() {
886                        return getSessionHolder().isRollbackOnly() ||
887                                        (hasConnectionHolder() && getConnectionHolder().isRollbackOnly());
888                }
889
890                @Override
891                public void flush() {
892                        try {
893                                getSessionHolder().getSession().flush();
894                        }
895                        catch (HibernateException ex) {
896                                throw convertHibernateAccessException(ex);
897                        }
898                        catch (PersistenceException ex) {
899                                if (ex.getCause() instanceof HibernateException) {
900                                        throw convertHibernateAccessException((HibernateException) ex.getCause());
901                                }
902                                throw ex;
903                        }
904                }
905        }
906
907
908        /**
909         * Holder for suspended resources.
910         * Used internally by {@code doSuspend} and {@code doResume}.
911         */
912        private static final class SuspendedResourcesHolder {
913
914                private final SessionHolder sessionHolder;
915
916                @Nullable
917                private final ConnectionHolder connectionHolder;
918
919                private SuspendedResourcesHolder(SessionHolder sessionHolder, @Nullable ConnectionHolder conHolder) {
920                        this.sessionHolder = sessionHolder;
921                        this.connectionHolder = conHolder;
922                }
923
924                private SessionHolder getSessionHolder() {
925                        return this.sessionHolder;
926                }
927
928                @Nullable
929                private ConnectionHolder getConnectionHolder() {
930                        return this.connectionHolder;
931                }
932        }
933
934}