From b3cc1b10341b37fc4cf802f829d3eb1b8d9626a5 Mon Sep 17 00:00:00 2001 From: sbohlen Date: Thu, 7 Oct 2010 20:04:41 +0000 Subject: [PATCH] SPRNET-1380 Introduced HibernateTxScopeTransactionManager class to coordinate rollback behavior between System.Transactions and NHibernate ITransaction instances based on changed made to NH in 2.1.2 --- .../HibernateTxScopeTransactionManager.cs | 1095 +++++++++++++++++ .../Spring.Data.NHibernate21.2010.csproj | 5 +- ...HibernateTxScopeTransactionManagerTests.cs | 139 +++ ...ibernateTxScopeTransactionManagerTests.xml | 88 ++ ...NHibernate21.Integration.Tests.2010.csproj | 2 + 5 files changed, 1327 insertions(+), 2 deletions(-) create mode 100644 src/Spring/Spring.Data.NHibernate21/Data/NHibernate/HibernateTxScopeTransactionManager.cs create mode 100644 test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.cs create mode 100644 test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.xml diff --git a/src/Spring/Spring.Data.NHibernate21/Data/NHibernate/HibernateTxScopeTransactionManager.cs b/src/Spring/Spring.Data.NHibernate21/Data/NHibernate/HibernateTxScopeTransactionManager.cs new file mode 100644 index 00000000..eea02dd9 --- /dev/null +++ b/src/Spring/Spring.Data.NHibernate21/Data/NHibernate/HibernateTxScopeTransactionManager.cs @@ -0,0 +1,1095 @@ +#region License + +/* + * Copyright © 2002-2005 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System; +using System.Data; +using System.Reflection; +using System.Transactions; +using NHibernate; +using NHibernate.Transaction; + +using Spring.Core.TypeResolution; +using Spring.Dao; +using Spring.Data.Common; +using Spring.Data.Core; +using Spring.Data.Support; +using Spring.Objects.Factory; +using Spring.Transaction; +using Spring.Transaction.Support; +using Spring.Util; +using HibernateTransactionException = NHibernate.TransactionException; + +#endregion + +namespace Spring.Data.NHibernate +{ + /// + /// PlatformTransactionManager implementation for a single Hibernate SessionFactory. + /// Binds a Hibernate Session from the specified factory to the thread, potentially + /// allowing for one thread Session per factory + /// + /// + /// SessionFactoryUtils and HibernateTemplate are aware of thread-bound Sessions and participate in such + /// transactions automatically. Using either of those is required for Hibernate + /// access code that needs to support this transaction handling mechanism. + /// + /// Supports custom isolation levels at the start of the transaction + /// , and timeouts that get applied as appropriate + /// Hibernate query timeouts. To support the latter, application code must either use + /// HibernateTemplate (which by default applies the timeouts) or call + /// SessionFactoryUtils.applyTransactionTimeout for each created + /// Hibernate Query object. + /// + /// Note that you can specify a Spring IDbProvider instance which if shared with + /// a corresponding instance of AdoTemplate will allow for mixing ADO.NET/NHibernate + /// operations within a single transaction. + /// + /// Mark Pollack (.NET) + public class HibernateTxScopeTransactionManager : AbstractPlatformTransactionManager, IResourceTransactionManager, IObjectFactoryAware, IInitializingObject + { + #region Fields + + private ISessionFactory sessionFactory; + + private IDbProvider dbProvider; + + private bool autodetectDbProvider = true; + + private Object entityInterceptor; + + private IAdoExceptionTranslator adoExceptionTranslator; + + private IAdoExceptionTranslator defaultExceptionTranslator; + + /// + /// Just needed for entityInterceptorBeanName. + /// + private IObjectFactory objectFactory; + + private TxScopeTransactionManager txScopeTranactionManager; + + #endregion + + #region Constructor (s) + + /// + /// Initializes a new instance of the class. + /// + public HibernateTxScopeTransactionManager() + { + txScopeTranactionManager = new TxScopeTransactionManager(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The session factory. + public HibernateTxScopeTransactionManager(ISessionFactory sessionFactory) + { + this.sessionFactory = sessionFactory; + AfterPropertiesSet(); + } + + #endregion + + #region Properties + + /// + /// Gets or sets the db provider. + /// + /// The db provider. + public IDbProvider DbProvider + { + get { return dbProvider; } + set { dbProvider = value; } + } + + /// + /// Gets or sets a Hibernate entity interceptor that allows to inspect and change + /// property values before writing to and reading from the database. + /// When getting, return the current Hibernate entity interceptor, or null if none. + /// + /// The entity interceptor. + /// + /// Resolves an entity interceptor object name via the object factory, + /// if necessary. + /// Will get applied to any new Session created by this transaction manager. + /// Such an interceptor can either be set at the SessionFactory level, + /// i.e. on LocalSessionFactoryObject, or at the Session level, i.e. on + /// HibernateTemplate, HibernateInterceptor, and HibernateTransactionManager. + /// It's preferable to set it on LocalSessionFactoryObject or HibernateTransactionManager + /// to avoid repeated configuration and guarantee consistent behavior in transactions. + /// + /// If object factory is null and need to get entity interceptor via object name. + public IInterceptor EntityInterceptor + { + get + { + if (this.entityInterceptor is IInterceptor) + { + return (IInterceptor)entityInterceptor; + } + else if (this.entityInterceptor is string) + { + if (this.objectFactory == null) + { + throw new InvalidOperationException("Cannot get entity interceptor via object name if no object factory set"); + } + String objectName = (String)this.entityInterceptor; + return (IInterceptor)this.objectFactory.GetObject(objectName, typeof(IInterceptor)); + } + else + { + return null; + } + } + set + { + entityInterceptor = value; + } + } + + /// + /// Sets the object name of a Hibernate entity interceptor that + /// allows to inspect and change property values before writing to and reading from the database. + /// + /// The name of the entity interceptor object. + /// + /// Will get applied to any new Session created by this transaction manager. + ///

Requires the object factory to be known, to be able to resolve the object + /// name to an interceptor instance on session creation. Typically used for + /// prototype interceptors, i.e. a new interceptor instance per session. + ///

+ ///

Can also be used for shared interceptor instances, but it is recommended + /// to set the interceptor reference directly in such a scenario. + ///

+ ///
+ public string EntityInterceptorObjectName + { + set + { + entityInterceptor = value; + } + } + + /// + /// Gets or sets the ADO.NET exception translator for this transaction manager. + /// + /// + /// Applied to ADO.NET Exceptions (wrapped by Hibernate's ADOException) + /// + /// The ADO exception translator. + public IAdoExceptionTranslator AdoExceptionTranslator + { + get { return adoExceptionTranslator; } + set { adoExceptionTranslator = value; } + } + + /// + /// Gets the default IAdoException translator, lazily creating it if nece + /// + /// The default IAdoException translator. + public IAdoExceptionTranslator DefaultAdoExceptionTranslator + { + get + { + lock (this) + { + if (defaultExceptionTranslator == null) + { + if (dbProvider != null) + { + defaultExceptionTranslator = new ErrorCodeExceptionTranslator(dbProvider); + } + else + { + defaultExceptionTranslator = SessionFactoryUtils.NewAdoExceptionTranslator(SessionFactory); + } + } + return defaultExceptionTranslator; + } + } + } + + /// + /// Gets or sets the SessionFactory that this instance should manage transactions for. + /// + /// The session factory. + public ISessionFactory SessionFactory + { + get { return sessionFactory; } + set { sessionFactory = value; } + } + + + /// + /// Gets the resource factory that this transaction manager operates on, + /// For the HibenratePlatformTransactionManager this the SessionFactory + /// + /// The SessionFactory. + public object ResourceFactory + { + get { return sessionFactory; } + } + + /// + /// Set whether to autodetect a ADO.NET connection used by the Hibernate SessionFactory, + /// if set via LocalSessionFactoryObject's DbProvider. Default is "true". + /// + /// + /// true if [autodetect data source]; otherwise, false. + /// + /// + ///

Can be turned off to deliberately ignore an available IDbProvider, + /// to not expose Hibernate transactions as ADO.NET transactions for that IDbProvider. + ///

+ ///
+ public bool AutodetectDbProvider + { + set { autodetectDbProvider = value; } + } + + #endregion + + #region Methods + + #endregion + + + /// + /// The object factory just needs to be known for resolving entity interceptor + /// It does not need to be set for any other mode of operation. + /// + /// + /// Owning + /// (may not be ). The object can immediately + /// call methods on the factory. + /// + public IObjectFactory ObjectFactory + { + set + { + objectFactory = value; + } + } + + /// + /// Return the current transaction object. + /// + /// The current transaction object. + /// + /// If transaction support is not available. + /// + /// + /// In the case of lookup or system errors. + /// + protected override object DoGetTransaction() + { + + + HibernateTransactionObject txObject = new HibernateTransactionObject(); + txObject.SavepointAllowed = NestedTransactionsAllowed; + if (TransactionSynchronizationManager.HasResource(SessionFactory)) + { + SessionHolder sessionHolder = + (SessionHolder)TransactionSynchronizationManager.GetResource(SessionFactory); + if (log.IsDebugEnabled) + { + log.Debug("Found thread-bound Session [" + sessionHolder.Session + + "] for Hibernate transaction"); + } + txObject.SetSessionHolder(sessionHolder, false); + if (DbProvider != null) + { + ConnectionHolder conHolder = (ConnectionHolder) + TransactionSynchronizationManager.GetResource(DbProvider); + txObject.ConnectionHolder = conHolder; + } + } + txObject.PromotableTxScopeTransactionObject = new TxScopeTransactionManager.PromotableTxScopeTransactionObject(); + + return txObject; + } + + /// + /// Check if the given transaction object indicates an existing, + /// i.e. already begun, transaction. + /// + /// + /// Transaction object returned by + /// . + /// + /// True if there is an existing transaction. + /// + /// In the case of system errors. + /// + protected override bool IsExistingTransaction(object transaction) + { + return + ((HibernateTransactionObject)transaction).PromotableTxScopeTransactionObject.TxScopeAdapter. + IsExistingTransaction; + //return ((HibernateTransactionObject) transaction).HasTransaction(); + } + + /// + /// Begin a new transaction with the given transaction definition. + /// + /// + /// Transaction object returned by + /// . + /// + /// + /// instance, describing + /// propagation behavior, isolation level, timeout etc. + /// + /// + /// Does not have to care about applying the propagation behavior, + /// as this has already been handled by this abstract manager. + /// + /// + /// In the case of creation or system errors. + /// + protected override void DoBegin(object transaction, ITransactionDefinition definition) + { + + TxScopeTransactionManager.PromotableTxScopeTransactionObject promotableTxScopeTransactionObject = + ((HibernateTransactionObject)transaction).PromotableTxScopeTransactionObject; + try + { + DoTxScopeBegin(promotableTxScopeTransactionObject, definition); + } + catch (Exception e) + { + throw new CannotCreateTransactionException("Transaction Scope failure on begin", e); + } + + HibernateTransactionObject txObject = (HibernateTransactionObject)transaction; + + if (DbProvider != null && TransactionSynchronizationManager.HasResource(DbProvider) + && !txObject.ConnectionHolder.SynchronizedWithTransaction) + { + throw new IllegalTransactionStateException( + "Pre-bound ADO.NET Connection found - HibernateTransactionManager does not support " + + "running within AdoTransactionManager if told to manage the DbProvider itself. " + + "It is recommended to use a single HibernateTransactionManager for all transactions " + + "on a single DbProvider, no matter whether Hibernate or ADO.NET access."); + } + ISession session = null; + try + { + + if (txObject.SessionHolder == null || txObject.SessionHolder.SynchronizedWithTransaction) + { + IInterceptor interceptor = EntityInterceptor; + ISession newSession = (interceptor != null ? + SessionFactory.OpenSession(interceptor) : SessionFactory.OpenSession()); + + if (log.IsDebugEnabled) + { + log.Debug("Opened new Session [" + newSession + "] for Hibernate transaction"); + } + txObject.SetSessionHolder(new SessionHolder(newSession), true); + + } + txObject.SessionHolder.SynchronizedWithTransaction = true; + session = txObject.SessionHolder.Session; + + IDbConnection con = session.Connection; + //TODO isolation level mgmt + //IsolationLevel previousIsolationLevel = + + if (definition.ReadOnly && txObject.NewSessionHolder) + { + // Just set to NEVER in case of a new Session for this transaction. + session.FlushMode = FlushMode.Never; + } + + if (!definition.ReadOnly && !txObject.NewSessionHolder) + { + // We need AUTO or COMMIT for a non-read-only transaction. + FlushMode flushMode = session.FlushMode; + if (FlushMode.Never == flushMode) + { + session.FlushMode = FlushMode.Auto; + txObject.SessionHolder.PreviousFlushMode = flushMode; + } + } + + // Add the Hibernate transaction to the session holder. + // for now pass in tx options isolation level. + ITransaction hibernateTx = session.BeginTransaction(definition.TransactionIsolationLevel); + IDbTransaction adoTx = GetIDbTransaction(hibernateTx); + + // Add the Hibernate transaction to the session holder. + txObject.SessionHolder.Transaction = hibernateTx; + + // Register transaction timeout. + int timeout = DetermineTimeout(definition); + if (timeout != DefaultTransactionDefinition.TIMEOUT_DEFAULT) + { + txObject.SessionHolder.TimeoutInSeconds = timeout; + } + + // Register the Hibernate Session's ADO.NET Connection/TX pair for the DbProvider, if set. + if (DbProvider != null) + { + //investigate passing null for tx. + ConnectionHolder conHolder = new ConnectionHolder(con, adoTx); + if (timeout != DefaultTransactionDefinition.TIMEOUT_DEFAULT) + { + conHolder.TimeoutInMillis = definition.TransactionTimeout; + } + if (log.IsDebugEnabled) + { + log.Debug("Exposing Hibernate transaction as ADO transaction [" + con + "]"); + } + TransactionSynchronizationManager.BindResource(DbProvider, conHolder); + txObject.ConnectionHolder = conHolder; + } + + // Bind the session holder to the thread. + if (txObject.NewSessionHolder) + { + TransactionSynchronizationManager.BindResource(SessionFactory, txObject.SessionHolder); + } + + } + catch (Exception ex) + { + SessionFactoryUtils.CloseSession(session); + throw new CannotCreateTransactionException("Could not open Hibernate Session for transaction", ex); + } + + + } + + private void DoTxScopeBegin(TxScopeTransactionManager.PromotableTxScopeTransactionObject txObject, + Spring.Transaction.ITransactionDefinition definition) + { + + TransactionScopeOption txScopeOption = CreateTransactionScopeOptions(definition); + TransactionOptions txOptions = CreateTransactionOptions(definition); + txObject.TxScopeAdapter.CreateTransactionScope(txScopeOption, txOptions, definition.EnterpriseServicesInteropOption); + + } + + private static TransactionScopeOption CreateTransactionScopeOptions(ITransactionDefinition definition) + { + TransactionScopeOption txScopeOption; + if (definition.PropagationBehavior == TransactionPropagation.Required) + { + txScopeOption = TransactionScopeOption.Required; + } + else if (definition.PropagationBehavior == TransactionPropagation.RequiresNew) + { + txScopeOption = TransactionScopeOption.RequiresNew; + } + else if (definition.PropagationBehavior == TransactionPropagation.NotSupported) + { + txScopeOption = TransactionScopeOption.Suppress; + } + else + { + throw new Spring.Transaction.TransactionSystemException("Transaction Propagation Behavior" + + definition.PropagationBehavior + + " not supported by TransactionScope. Use Required or RequiredNew"); + } + return txScopeOption; + } + + + private static TransactionOptions CreateTransactionOptions(ITransactionDefinition definition) + { + TransactionOptions txOptions = new TransactionOptions(); + switch (definition.TransactionIsolationLevel) + { + case System.Data.IsolationLevel.Chaos: + txOptions.IsolationLevel = System.Transactions.IsolationLevel.Chaos; + break; + case System.Data.IsolationLevel.ReadCommitted: + txOptions.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted; + break; + case System.Data.IsolationLevel.ReadUncommitted: + txOptions.IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted; + break; + case System.Data.IsolationLevel.RepeatableRead: + txOptions.IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead; + break; + case System.Data.IsolationLevel.Serializable: + txOptions.IsolationLevel = System.Transactions.IsolationLevel.Serializable; + break; + case System.Data.IsolationLevel.Snapshot: + txOptions.IsolationLevel = System.Transactions.IsolationLevel.Snapshot; + break; + case System.Data.IsolationLevel.Unspecified: + txOptions.IsolationLevel = System.Transactions.IsolationLevel.Unspecified; + break; + } + + if (definition.TransactionTimeout != DefaultTransactionDefinition.TIMEOUT_DEFAULT) + { + txOptions.Timeout = new TimeSpan(0, 0, definition.TransactionTimeout); + } + return txOptions; + } + + + + /// + /// Suspend the resources of the current transaction. + /// + /// + /// Transaction object returned by + /// . + /// + /// + /// An object that holds suspended resources (will be kept unexamined for passing it into + /// .) + /// + /// + /// Transaction synchronization will already have been suspended. + /// + /// + /// If suspending is not supported by the transaction manager implementation. + /// + /// + /// in case of system errors. + /// + protected override object DoSuspend(object transaction) + { + HibernateTransactionObject txObject = (HibernateTransactionObject)transaction; + txObject.SetSessionHolder(null, false); + SessionHolder sessionHolder = + (SessionHolder)TransactionSynchronizationManager.UnbindResource(SessionFactory); + ConnectionHolder connectionHolder = null; + if (DbProvider != null) + { + connectionHolder = (ConnectionHolder)TransactionSynchronizationManager.UnbindResource(DbProvider); + } + return new SuspendedResourcesHolder(sessionHolder, connectionHolder); + + } + + /// + /// Resume the resources of the current transaction. + /// + /// + /// Transaction object returned by + /// . + /// + /// + /// The object that holds suspended resources as returned by + /// . + /// + /// + /// Transaction synchronization will be resumed afterwards. + /// + /// + /// If suspending is not supported by the transaction manager implementation. + /// + /// + /// In the case of system errors. + /// + protected override void DoResume(object transaction, object suspendedResources) + { + SuspendedResourcesHolder resourcesHolder = (SuspendedResourcesHolder)suspendedResources; + if (TransactionSynchronizationManager.HasResource(SessionFactory)) + { + // From non-transactional code running in active transaction synchronization + // -> can be safely removed, will be closed on transaction completion. + TransactionSynchronizationManager.UnbindResource(SessionFactory); + } + TransactionSynchronizationManager.BindResource(SessionFactory, resourcesHolder.SessionHolder); + if (DbProvider != null) + { + TransactionSynchronizationManager.BindResource(DbProvider, resourcesHolder.ConnectionHolder); + } + } + + /// + /// Perform an actual commit on the given transaction. + /// + /// The status representation of the transaction. + /// + ///

+ /// An implementation does not need to check the rollback-only flag. + ///

+ ///
+ /// + /// In the case of system errors. + /// + protected override void DoCommit(DefaultTransactionStatus status) + { + HibernateTransactionObject txObject = (HibernateTransactionObject)status.Transaction; + if (status.Debug) + { + log.Debug("Committing Hibernate transaction on Session [" + + txObject.SessionHolder.Session + "]"); + } + try + { + txObject.SessionHolder.Transaction.Commit(); + } + // Note, unfortunate collision of namespaces/classname for NHibernate.TransactionException + // and Spring.Data.NHibernate requires this wierd construct. + catch (Exception ex) + { + Type nhibTxExceptiontype = TypeResolutionUtils.ResolveType("NHibernate.TransactionException, NHibernate"); + if (ex.GetType().Equals(nhibTxExceptiontype)) + { + // assumably from commit call to the underlying ADO.NET connection + throw new TransactionSystemException("Could not commit Hibernate transaction", ex); + } + HibernateException hibEx = ex as HibernateException; + if (hibEx != null) + { + // assumably failed to flush changes to database + throw ConvertHibernateAccessException(hibEx); + } + throw; + } + finally + { + DoTxScopeCommit(status); + } + + + + } + + protected void DoTxScopeCommit(DefaultTransactionStatus status) + { + TxScopeTransactionManager.PromotableTxScopeTransactionObject txObject = + ((HibernateTransactionObject)status.Transaction).PromotableTxScopeTransactionObject; + try + { + txObject.TxScopeAdapter.Complete(); + txObject.TxScopeAdapter.Dispose(); + } + catch (TransactionAbortedException ex) + { + throw new UnexpectedRollbackException("Transaction unexpectedly rolled back (maybe due to a timeout)", ex); + } + catch (TransactionInDoubtException ex) + { + throw new HeuristicCompletionException(TransactionOutcomeState.Unknown, ex); + } + catch (Exception ex) + { + throw new TransactionSystemException("Failure on Transaction Scope Commit", ex); + } + } + + /// + /// Perform an actual rollback on the given transaction. + /// + /// The status representation of the transaction. + /// + /// An implementation does not need to check the new transaction flag. + /// + /// + /// In the case of system errors. + /// + protected override void DoRollback(DefaultTransactionStatus status) + { + HibernateTransactionObject txObject = (HibernateTransactionObject)status.Transaction; + if (status.Debug) + { + log.Debug("Rolling back Hibernate transaction on Session [" + + txObject.SessionHolder.Session + "]"); + } + try + { + txObject.SessionHolder.Transaction.Rollback(); + } + catch (HibernateTransactionException ex) + { + throw new TransactionSystemException("Could not roll back Hibernate transaction", ex); + } + catch (HibernateException ex) + { + // Shouldn't really happen, as a rollback doesn't cause a flush. + throw ConvertHibernateAccessException(ex); + } + finally + { + if (!txObject.NewSessionHolder) + { + // Clear all pending inserts/updates/deletes in the Session. + // Necessary for pre-bound Sessions, to avoid inconsistent state. + txObject.SessionHolder.Session.Clear(); + } + DoTxScopeRollback(status); + } + } + + protected void DoTxScopeRollback(DefaultTransactionStatus status) + { + TxScopeTransactionManager.PromotableTxScopeTransactionObject txObject = + ((HibernateTransactionObject)status.Transaction).PromotableTxScopeTransactionObject; + + try + { + + txObject.TxScopeAdapter.Dispose(); + } + catch (Exception e) + { + throw new Spring.Transaction.TransactionSystemException("Failure on Transaction Scope rollback.", e); + } + } + + /// + /// Set the given transaction rollback-only. Only called on rollback + /// if the current transaction takes part in an existing one. + /// + /// The status representation of the transaction. + /// + /// In the case of system errors. + /// + protected override void DoSetRollbackOnly(DefaultTransactionStatus status) + { + HibernateTransactionObject txObject = (HibernateTransactionObject)status.Transaction; + if (status.Debug) + { + log.Debug("Setting Hibernate transaction on Session [" + + txObject.SessionHolder.Session + "] rollback-only"); + } + txObject.SetRollbackOnly(); + + DoTxScopeSetRollbackOnly(status); + } + + protected void DoTxScopeSetRollbackOnly(DefaultTransactionStatus status) + { + if (status.Debug) + { + log.Debug("Setting transaction rollback-only"); + } + try + { + System.Transactions.Transaction.Current.Rollback(); + } + catch (Exception ex) + { + throw new TransactionSystemException("Failure on System.Transactions.Transaction.Current.Rollback", ex); + } + } + + + /// + /// Gets the ADO.NET IDbTransaction object from the NHibernate ITransaction object. + /// + /// The hibernate transaction. + /// The ADO.NET transaction. Null if could not get the transaction. Warning + /// messages will be logged in that case. + protected IDbTransaction GetIDbTransaction(ITransaction hibernateTx) + { + AdoTransaction hibernateAdoTx = hibernateTx as AdoTransaction; + + IDbTransaction adoTransaction = null; + if (hibernateAdoTx != null) + { + try + { + FieldInfo fi = hibernateAdoTx.GetType().GetField("trans", BindingFlags.Instance | BindingFlags.NonPublic); + adoTransaction = fi.GetValue(hibernateAdoTx) as IDbTransaction; + } + catch (Exception e) + { + log.Warn("Could not extract IDbTransaction from Hibernate AdoTransaction using field name trans.", e); + } + } + else + { + log.Warn("Hibernate ITransaction not of expected type AdoTransaction. Could not extract IDbTransaction from Hibernate AdoTransaction."); + } + return adoTransaction; + } + + /// + /// Convert the given HibernateException to an appropriate exception from + /// the Spring.Dao hierarchy. Can be overridden in subclasses. + /// + /// The HibernateException that occured. + /// The corresponding DataAccessException instance + protected virtual DataAccessException ConvertHibernateAccessException(HibernateException ex) + { + if (AdoExceptionTranslator != null && ex is ADOException) + { + return ConvertAdoAccessException((ADOException)ex, AdoExceptionTranslator); + } + else if (ex is ADOException) + { + return ConvertAdoAccessException((ADOException)ex, DefaultAdoExceptionTranslator); + } + return SessionFactoryUtils.ConvertHibernateAccessException(ex); + } + + /// + /// Convert the given ADOException to an appropriate exception from the + /// the Spring.Dao hierarchy. Can be overridden in subclasses. + /// + /// The ADOException that occured, wrapping the underlying + /// ADO.NET thrown exception. + /// The translator to convert hibernate ADOExceptions. + /// + /// The corresponding DataAccessException instance + /// + protected virtual DataAccessException ConvertAdoAccessException(ADOException ex, IAdoExceptionTranslator translator) + { + return translator.Translate("Hibernate flusing: " + ex.Message, null, ex.InnerException); + } + + /// + /// Cleanup resources after transaction completion. + /// + /// Transaction object returned by + /// . + /// + /// + /// This implemenation unbinds the SessionFactory and + /// DbProvider from thread local storage and closes the + /// ISession. + /// + ///

+ /// Called after + /// and + /// + /// execution on any outcome. + ///

+ ///

+ /// Should not throw any exceptions but just issue warnings on errors. + ///

+ ///
+ protected override void DoCleanupAfterCompletion(object transaction) + { + HibernateTransactionObject txObject = (HibernateTransactionObject)transaction; + + // Remove the session holder from the thread. + if (txObject.NewSessionHolder) + { + TransactionSynchronizationManager.UnbindResource(SessionFactory); + } + // Remove the ADO.NET connection holder from the thread, if exposed. + if (DbProvider != null) + { + TransactionSynchronizationManager.UnbindResource(DbProvider); + } + /* + try + { + //TODO investigate isolation level settings... + //IDbConnection con = txObject.SessionHolder.Session.Connection; + //AdoUtils.ResetConnectionAfterTransaction(con, txObject.PreviousIsolationLevel); + } + catch (HibernateException ex) + { + log.Info("Could not access ADO.NET IDbConnection of Hibernate Session", ex); + } + */ + ISession session = txObject.SessionHolder.Session; + if (txObject.NewSessionHolder) + { + if (log.IsDebugEnabled) + { + log.Debug("Closing Hibernate Session [" + session + "] after transaction"); + } + SessionFactoryUtils.CloseSessionOrRegisterDeferredClose(session, SessionFactory); + } + else + { + if (log.IsDebugEnabled) + { + log.Debug("Not closing pre-bound Hibernate Session [" + session + "] after transaction"); + } + if (txObject.SessionHolder.AssignedPreviousFlushMode) + { + session.FlushMode = txObject.SessionHolder.PreviousFlushMode; + } + } + txObject.SessionHolder.Clear(); + + + } + + private class HibernateTransactionObject : AdoTransactionObjectSupport + { + + private SessionHolder sessionHolder; + + private bool newSessionHolder; + + private TxScopeTransactionManager.PromotableTxScopeTransactionObject promotableTxScopeTransactionObject; + + + public void SetSessionHolder(SessionHolder sessionHolder, bool newSessionHolder) + { + this.sessionHolder = sessionHolder; + this.newSessionHolder = newSessionHolder; + } + + public TxScopeTransactionManager.PromotableTxScopeTransactionObject PromotableTxScopeTransactionObject + { + get { return promotableTxScopeTransactionObject; } + set { this.promotableTxScopeTransactionObject = value; } + } + + public SessionHolder SessionHolder + { + get + { + return sessionHolder; + } + } + + public bool NewSessionHolder + { + get + { + return newSessionHolder; + } + } + + public bool HasTransaction() + { + return (this.sessionHolder != null && this.sessionHolder.Transaction != null); + } + + public void SetRollbackOnly() + { + SessionHolder.RollbackOnly = true; + if (ConnectionHolder != null) + { + ConnectionHolder.RollbackOnly = true; + } + } + + /// + /// Return whether the transaction is internally marked as rollback-only. + /// + /// + /// True of the transaction is marked as rollback-only. + public override bool RollbackOnly + { + get + { + return SessionHolder.RollbackOnly || + (ConnectionHolder != null && ConnectionHolder.RollbackOnly); + } + } + } + + private class SuspendedResourcesHolder + { + + private readonly SessionHolder sessionHolder; + + private readonly ConnectionHolder connectionHolder; + + public SuspendedResourcesHolder(SessionHolder sessionHolder, ConnectionHolder conHolder) + { + this.sessionHolder = sessionHolder; + this.connectionHolder = conHolder; + } + + public SessionHolder SessionHolder + { + get + { + return sessionHolder; + } + + } + + public ConnectionHolder ConnectionHolder + { + get + { + return connectionHolder; + } + + } + } + + /// + /// Invoked by an + /// after it has injected all of an object's dependencies. + /// + /// + ///

+ /// This method allows the object instance to perform the kind of + /// initialization only possible when all of it's dependencies have + /// been injected (set), and to throw an appropriate exception in the + /// event of misconfiguration. + ///

+ ///

+ /// Please do consult the class level documentation for the + /// interface for a + /// description of exactly when this method is invoked. In + /// particular, it is worth noting that the + /// + /// and + /// callbacks will have been invoked prior to this method being + /// called. + ///

+ ///
+ /// + /// In the event of misconfiguration (such as the failure to set a + /// required property) or if initialization fails. + /// + public void AfterPropertiesSet() + { + if (SessionFactory == null) + { + throw new ArgumentException("sessionFactory is required"); + } + if (this.entityInterceptor is string && this.objectFactory == null) + { + throw new ArgumentException("objectFactory is required for entityInterceptorBeanName"); + } + + // Try to derive a DbProvider given the SessionFactory. + if (this.autodetectDbProvider && DbProvider == null) + { + IDbProvider sfDbProvider = SessionFactoryUtils.GetDbProvider(SessionFactory); + if (sfDbProvider != null) + { + // Use the SessionFactory's DataSource for exposing transactions to ADO.NET code. + if (log.IsInfoEnabled) + { + log.Info("Derived DbProvider [" + sfDbProvider.DbMetadata.ProductName + + "] of Hibernate SessionFactory for HibernateTransactionManager"); + } + DbProvider = sfDbProvider; + } + else + { + log.Info("Could not auto detect DbProvider from SessionFactory configuration"); + } + + } + } + } + + +} diff --git a/src/Spring/Spring.Data.NHibernate21/Spring.Data.NHibernate21.2010.csproj b/src/Spring/Spring.Data.NHibernate21/Spring.Data.NHibernate21.2010.csproj index 205f99f7..88c67707 100644 --- a/src/Spring/Spring.Data.NHibernate21/Spring.Data.NHibernate21.2010.csproj +++ b/src/Spring/Spring.Data.NHibernate21/Spring.Data.NHibernate21.2010.csproj @@ -26,8 +26,7 @@ 4 Spring.Data.NHibernate21.xml true - - + 1591 pdbonly @@ -60,6 +59,7 @@ + @@ -170,6 +170,7 @@ + diff --git a/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.cs b/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.cs new file mode 100644 index 00000000..7b963665 --- /dev/null +++ b/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.cs @@ -0,0 +1,139 @@ +#region License + +/* + * Copyright © 2002-2005 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +#region Imports + +using System; +using System.Data; +using log4net; +using log4net.Config; +using NHibernate; +using NUnit.Framework; +using Spring.Context; +using Spring.Context.Support; +using Spring.Data.Common; +using Spring.Data.Support; +using Spring.Transaction; +using Spring.Transaction.Support; +using Spring.Transaction.Interceptor; +using System.Transactions; + +#endregion + +namespace Spring.Data.NHibernate +{ + /// + /// Use of Hibernate Template against database. + /// + /// Mark Pollack (.NET) + [TestFixture] + public class HibernateTxScopeTransactionManagerTests + { + #region Fields + private IDbProvider dbProvider; + + private IPlatformTransactionManager transactionManager; + + private IApplicationContext ctx; + #endregion + + #region Constants + + /// + /// The shared instance for this class (and derived classes). + /// + protected static readonly ILog log = + LogManager.GetLogger(typeof(TemplateTests)); + + //// force Spring.Data.NHibernate to be preloaded by runtime + //private Type TLocalSessionFactoryObject = typeof(LocalSessionFactoryObject); + + #endregion + + [SetUp] + public void SetUp() + { + //NamespaceParserRegistry.RegisterParser(typeof(DatabaseNamespaceParser)); + BasicConfigurator.Configure(); + string assemblyName = GetType().Assembly.GetName().Name; + ctx = new XmlApplicationContext("assembly://" + assemblyName + "/Spring.Data.NHibernate/HibernateTxScopeTransactionManagerTests.xml"); + + dbProvider = ctx["DbProvider"] as IDbProvider; + transactionManager = ctx["transactionManager"] as IPlatformTransactionManager; + CleanupDatabase(dbProvider.CreateConnection()); + } + + private static void CleanupDatabase(IDbConnection conn) + { + conn.Open(); + using (conn) + { + ExecuteSql(conn, "delete credits"); + ExecuteSql(conn, "delete debits"); + ExecuteSql(conn, "delete TestObjects"); + ExecuteSql(conn, "insert TestObjects(Age,Name) Values(5, 'Gabriel')"); + } + } + + private static void ExecuteSql(IDbConnection conn, string sql) + { + IDbCommand cmd; + cmd = conn.CreateCommand(); + cmd.CommandText = sql; + cmd.ExecuteNonQuery(); + } + + + [Transaction] + private void DoSave(bool simulateException) + { + using (TransactionScope tx = new TransactionScope()) + { + ISession s = ((ISessionFactory)ctx["SessionFactory"]).OpenSession(); + + TestObject to = new TestObject(); + to.Name = "George"; + to.Age = 33; + + if (simulateException) { throw new Exception("Simulated Failure in Save Operation."); } + + s.Save(to); + + tx.Complete(); + } + + } + + [Test] + public void CanProperlyReleaseConnectionsWhenTransactionsAreRolledBack() + { + for (int i = 0; i < 200; i++) + { + try + { + DoSave(true); + } + catch (Exception) + { + } + } + } + } +} diff --git a/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.xml b/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.xml new file mode 100644 index 00000000..8e82d88f --- /dev/null +++ b/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Data/NHibernate/HibernateTxScopeTransactionManagerTests.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + assembly://Spring.Data.NHibernate21.Integration.Tests/Spring.Data.NHibernate/TestObject.hbm.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Spring.Data.NHibernate21.Integration.Tests.2010.csproj b/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Spring.Data.NHibernate21.Integration.Tests.2010.csproj index 83f0d337..1b157903 100644 --- a/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Spring.Data.NHibernate21.Integration.Tests.2010.csproj +++ b/test/Spring/Spring.Data.NHibernate21.Integration.Tests/Spring.Data.NHibernate21.Integration.Tests.2010.csproj @@ -144,6 +144,7 @@ + @@ -202,6 +203,7 @@ + Always