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