65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ADO.NET 且无需企业服务的声明式事务

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (18投票s)

2001年10月23日

8分钟阅读

viewsIcon

219994

downloadIcon

1501

有时,为简单的数据库应用程序制作原型是件好事。此代码通过在非 COM+ 环境中提供 COM+ 的自动事务模型,可能会有所帮助。本示例使用“拦截”为非 COM+ 类提供自动事务支持。

摘要与动机

我正在开发一个简单的内部数据库应用程序,它不需要具备成熟的企业级、高安全性、多线程、对象池、即时激活(JIT Activated)、异构分布式事务应用程序的全部要求(呼~)。它仅使用 SQL Server 作为后端。它不必扩展到支持海量用户。这是一个相当典型的内部简单数据库开发项目。我想要一些 COM+ 的优点,比如声明式事务,换句话说,我希望编写像下面这样简单易读的代码:

[DbConnection]
[Transaction(TransactionOption.Required)]
sealed public class Employees : ContextBoundObject
{
    [AutoComplete]
    public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet
    {
        return (EmployeeDataSet)Sql.RunSP("Employee_GetById", 
                "Employees", 
                new EmployeeDataSet(), 
                Sql.CreateParameterWithValue("@EmployeeID", nID));
    }
}

但我不想面对生成强名称、COM+ 注册、配置等与 COM+ 企业模型相关的所有“麻烦”和“额外步骤”。我知道 .NET 环境下的 COM+(企业级)支持非常易于使用和配置……但对于这样一个简单的项目来说,它似乎还是增加了一些额外的部署工作。同时,我也对学习更多关于 .NET 环境的知识感兴趣,从这个角度来看,我的动机就更有意义了,因为很明显我并没有节省任何时间。*笑*。闲话少说,我最终实现的是对 SQL Server 托管提供程序的“自动事务”支持和额外的“连接”支持(尽管所提供的源代码可以轻松修改以支持其他托管提供程序)。

这样你就可以像上面那样编写代码,而不是像下面这样:

 sealed public class Employees
{
    public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet
    {
        SqlConnection Connection
            = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["dsn"]);
        Connection.Open();
        try
        {
            IDataParameter[] parameters =
            {
                SQL.CreateParameterWithValue("@EmployeeID", nID)
            };
            
            return (EmployeeDataSet)SQL.RunSP("Employee_GetById", parameters, 
                                              "Employees", new EmployeeDataSet(), 
                                              Connection);
        }
        finally
        {
              Connection.Dispose();
              Connection = null;
        }
    }
}

这段代码稍长一些,更难阅读,而且仍然缺少手动事务支持。声明式自动事务的真正好处在于其因果效应,即 A 调用 B,B 调用 C,如果 C 失败,你希望中止整个事务。或者在你事先不知道你的对象将如何组合成事务单元的情况下,它也很有用。

它是如何工作的?

第一步是根据声明的特性(attribute)以某种方式将对象划分到事务中。因此,我们可以为一个类编写注解 [Transaction(TransactionOption.Required)],在运行时就会创建一个适当的事务“上下文(Context)”。这个自定义特性将是一个“”特性,它使用“拦截(Interception)”来挂钩类的方法,以便我们可以将事务支持注入到类中。我还决定支持 [AutoComplete] 这个“方法”特性。此外,我创建了一个 [DbConnection] 特性来帮助自动化数据库连接。那么,让我们深入探讨吧。

上下文与类特性

我想在深入之前,我们必须简要地谈谈 .NET 的“上下文”。.NET Framework 文档对上下文的定义如下:上下文是一个有序的属性序列,它为驻留于其中的对象定义了一个环境。对于那些被配置为需要某些自动服务(如同步、事务、即时激活、安全性等)的对象,在激活过程中会创建上下文。多个对象可以存在于一个上下文中。哎呀!这听起来很吓人,但实际上,这是一个非常酷且非常有用的概念。基本上,它是一种将共享某些运行时属性的对象组合在一起的方法(不确定这样说是否更好 *笑*)。无论如何,上下文允许我们声明所有这些对象将共享此事务,或者这类对象总是需要一个单独的事务。上下文使我们能够区分并因此划分对象。

如何编写自定义特性的所有细节超出了本文的范围,但本质上它涉及从 System.Attribute 类之一派生并提供您自己的功能。让我们的 [Transaction] 特性工作的关键是派生自 System.Runtime.Remoting.Contexts.ContextAttribute 类,如下所示:

public enum TransactionOption // The transaction options.
{ 
    Disabled = 0,
    NotSupported,
    Required,
    RequiresNew,
    Supported
}
 
[AttributeUsage(AttributeTargets.Class)]
public class TransactionAttribute : ContextAttribute
{
    private TransactionOption transactionOption;
 
    public TransactionAttribute(TransactionOption transactionOption)
        : base("Transaction")
    {
        this.transactionOption = transactionOption; // Store the TransactionOption for later.
    }
    ...

并实现方法 bool IsContextOK(Context ctx, IConstructionCallMessage ctor)。运行时会调用此方法,以检查此对象的上下文是否与传递给该方法的上下文兼容。因此……这允许我们根据声明的特性来区分我们的对象。

    public override boolIsContextOK(Context ctx, IConstructionCallMessage ctor)
    {
        if(transactionOption == TransactionOption.RequiresNew) // This class always 
                                                               // requires a new Context 
            return false;
 
        TransactionProperty transactionProperty
                = ctx.GetProperty(Transaction.PropertyName) as TransactionProperty;
        if(transactionOption == TransactionOption.Required) 
        {
            // If there is no existing transaction context then create a new one
            if(transactionProperty == null) 
                return false;
        }
        return true; // The current context is fine!!
    }
    ...

好……到目前为止一切顺利。我们可以根据声明的事务来区分和创建上下文。在继续讨论如何挂钩我们类的方法以注入事务支持之前,我们还必须讨论另一个主题。

上下文属性

上下文,像大多数其他对象一样,拥有定义和保存其状态的属性。好消息是这些属性可以由用户自定义。例如,我希望在上下文中存储一个属性,以便我能判断该上下文是否已经关联了一个事务。在很多方面,这相当于 ASP.NET 中的“<st1:place><st1:placename>会话<st1:placetype>状态”或任何其他类型的“名称-值字典”式查找。.NET Framework 提供了一种方法,让您可以在上下文中存储自定义属性,这是通过调用 System.ContextAttribute 类的以下方法实现的:

public virtual void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg) of the System.ContextAttribute class. So our implementation is as follows

public override void GetPropertiesForNewContext(IConstructionCallMessage ctor)
{
    ctor.ContextProperties.Add(this);
}

唯一的要求是该类实现 IContextProperty 接口,幸运的是 System.ContextAttribute 已经这样做了。

拦截(终于讲到了)

嗯……我漏掉了一小部分。为了让你的类参与到这个“上下文”机制中,你必须将你的类标记为上下文绑定类(这意味着它在上下文中运行)。要做到这一点,你的类必须派生自 System.ContextBound。好的。我想我们现在已经掌握了所有的要素……当你创建一个类的新实例时,根据我的理解,过程如下:

  1. ContextAttribute.IsContextOK() 方法被调用。如果返回 true,则该类在传递给它的上下文中创建。
  2. 如果返回 false,则会创建一个新的上下文。
  3. 然后系统会在新创建的上下文上调用 GetPropertiesForNewContext()。我们可以在这里将任何新的“属性”附加到上下文中。
  4. 对于每一个属性,系统会测试你是否实现了 System.Runtime.Remoting.Contexts.IContributeObjectSink 接口。
  5. 此接口包含方法 IMessageSink GetObjectSink(MarshalByRefObjectobj, IMessageSink nextSink),它允许你链接一个自定义的 IMessageSink 接口,从而可以拦截对该对象的方法调用。

下面是一些示例代码,GetObjectSink 的实现如下:

...
public IMessageSink GetObjectSink(MarshalByRefObject o, IMessageSinkm_Next)
{
    TransactionAttribute transactionProperty = Transaction.ContextProperty;
    if(transactionProperty != null)
    {
        return new DbConnectionMessageSink(this, 
            new TransactionMessageSink(transactionProperty, m_Next));
    }

    return new DbConnectionMessageSink(this, m_Next);
}

值得注意的一点是,该实现总是先链接 Connection 特性,以便它的方法挂钩在事务挂钩之前被调用。创建新实例时,即使将 Connection 特性放在 Transaction 特性之前,也无法保证它会先被调用。我们需要一种方法来控制事物的顺序,否则我们可能会在没有相应打开连接的情况下尝试创建事务。你总是希望(蓝色部分为注入的代码):

Connection.OpenConnection()
Transaction.Begin()
MethodCallGoesHere()
Transaction.Commit()
Connection.CloseConnection()

要完成拦截的最后一击,是 IMessageSink 接口的实际实现。当上下文中的对象上的方法被调用时,该方法会通过 IMessageSink 接口进行重定向。正如我们所发现的,这实际上是一个 IMessageSink 实现链,我们的挂钩就是其中之一。对于所有 COM/Remoting 的爱好者来说,这闻起来有点像代理/存根(Proxy/Stub)。总之,我们感兴趣的 IMessageSink 接口上的主要方法是 public IMessageSync ProcessMessage(IMessageimCall)。通过实现此方法,它允许我们挂钩“同步”方法调用。TransactionAttribute 消息接收器的实际实现如下:

public class TransactionMessageSink : IMessageSink
{
    private IMessageSink          m_Next;
    private TransactionAttribute  m_TransactionAttribute;
 
    internal TransactionMessageSink(TransactionAttribute transactionProperty, IMessageSinkims)
    {
        m_Next = ims;
        m_TransactionAttribute = transactionProperty;
    } 
 
    public IMessageSink NextSink
    {
        get { returnm_Next; }
    }
 
    public IMessageSync ProcessMessage(IMessageimCall)
    {
        // Perform whatever preprocessing is needed on the message
        if (!(imCallisIMethodMessage))
            returnm_Next.SyncProcessMessage(imCall);
 
        IMethodMessage imm = imCall as IMethodMessage;
        bool bAutoComplete = (Attribute.GetCustomAttribute(imm.MethodBase, 
                    typeof(AutoCompleteAttribute)) != null);
        m_TransactionAttribute.DisableCommit();
 
        SqlConnection Connection = (SqlConnection)DbConnectionAttribute.Connection;
        if(Connection == null)
            return m_Next.SyncProcessMessage(imCall);
 
#if DEBUGGING_TRXS
        Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" + 
                          " Beginning Transaction...");
#endif
        SqlTransaction dbTransaction = m_TransactionAttribute.DbTransaction
             = Connection.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted);
 
        // Dispatch the call on the object
        IMessage imReturn = m_Next.SyncProcessMessage(imCall);
 
        if(dbTransaction != null)
        {
            IMethodReturnMessage methodReturn = imReturn as IMethodReturnMessage;
            Exception exc = methodReturn.Exception;
            if (exc != null)
            {
                m_TransactionAttribute.SetAbort();
            }
            else
            {
                if(bAutoComplete)
                      m_TransactionAttribute.SetComplete();
            }
 
            if(!m_TransactionAttribute.Done)
                      m_TransactionAttribute.SetAbort();
 
            if(m_TransactionAttribute.ContextConsistent)
            {
#if DEBUGGING_TRXS
                  Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" + 
                                    " Committing Transaction...");
#endif
                  dbTransaction.Commit();
            }
            else
            {
#if DEBUGGING_TRXS
                  Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" + 
                                    " Aborting Transaction...");
#endif
                  dbTransaction.Rollback();
            }
 
            dbTransaction.Dispose();
            dbTransaction = null;
        }
 
        returnimReturn;
    } 
 
    public IMessageCtrlAsync ProcessMessage(IMessage im, IMessageSink ims)
    {
        // TODO: Find some way to also allow AsyncMessages to work (and ideally, be tracked)
        return m_Next.AsyncProcessMessage(im, ims);
    } 
}

我的天,这代码真多。一个关键部分是 IMessage imReturn = m_Next.SyncProcessMessage(imCall);,这个调用将方法调用转发给对象。另一部分只是将事务包装在该调用周围。

整合

文章的标题说是简单的,但我们至今还没看到任何简单的东西。这是因为“简单”指的是“客户端”这边(理应如此)。那么,让我们重新审视一下文章开头的例子,看看我们免费得到了什么。

[DbConnection]
[Transaction(TransactionOption.Required)]
sealed public class Employees : ContextBoundObject
{
    [AutoComplete]
    public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet
    {
        return (EmployeeDataSet)Sql.RunSP("Employee_GetById", 
            "Employees", new EmployeeDataSet(), 
            Sql.CreateParameterWithValue("@EmployeeID", nID));
        }
    }

我们有一个 [DbConnection] 特性,它为我们提供与数据库的连接。我们通过 [Transaction] 特性获得了自动事务支持。我们还有 [AutoComplete] 特性,如果方法调用期间一切顺利,即没有异常,事务就可以自动提交。让我们看看 Sql.RunSP() 方法,了解在托管 SQL Server 内部是如何访问 ConnectionTransaction 的。

static public DataSetRunSP(string procName, string tableName, DataSet dataSet, 
                           params IDataParameter[] parameters)
{
    SqlConnection dbConnection     = (SqlConnection)DbConnectionUtil.Connection;
        SqlTransaction dbTransaction   = (SqlTransaction)ContextUtil.DbTransaction;
        if((dbConnection == null) || (dbTransaction == null))
            throw new System.Exception("No Connection!");
 
        SqlDataAdapter DSCommand = new SqlDataAdapter();
        DSCommand.SelectCommand    = newSqlCommand(procName, dbConnection, dbTransaction);
        DSCommand.SelectCommand.CommandType   = CommandType.StoredProcedure;
        
        if(parameters != null)
        {
            foreach ( SqlParameter parameter in parameters )
                      DSCommand.SelectCommand.Parameters.Add( parameter );
        }
 
        DSCommand.Fill(dataSet, tableName);
        return dataSet;
    }

如你所见,有两个辅助类分别用于访问当前连接和当前事务,它们是 DbConnectionUtilContextUtil

实现说明

  • 当前支持 SQL Server 托管提供程序
  • 仅支持同步方法
  • 拦截、上下文等会带来一些开销,但连接和事务的开销和成本可能超过了这些。
  • 没有时间提供完整的文档和示例
  • 未完全调试。此代码应用作学习工具,未经全面测试不应在生产环境中使用。(重申显而易见的事实)

我希望这能帮助到某些人,我很乐意接收任何错误修复、功能增强等。

历史

  • 2001年10月21日:初始版本

许可证

本文没有附加明确的许可证,但可能在文章文本或下载文件中包含使用条款。如有疑问,请通过下面的讨论区联系作者。作者可能使用的许可证列表可以在这里找到。

© . All rights reserved.