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

数据访问组件 - 无需EnterpriseServices,在方法级别声明式事务,支持不同数据源,第1部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (16投票s)

2001年12月19日

9分钟阅读

viewsIcon

264208

downloadIcon

1725

数据访问组件 - ADO.NET的精简包装器,无需COM+提供的事务支持,为不同的数据源提供统一接口,从而允许最终轻松切换到另一个数据源

Sample Image - transactions.gif

我假设读者熟悉多层应用程序设计、数据库、XML、COM+、C#和.NET Framework。

感谢Sandy Place撰写的精彩文章使用ADO.NET而无需Enterprise Services的声明式事务,它最初启发了我开发这个数据访问组件(DAC),也感谢microsoft.public.dotnet新闻组的贡献者!

由于组件的功能扩展了很多,我将文章分为两部分——第一部分介绍由MsSqlData和OleDbData实现的通用接口方法、事务支持等,第二部分介绍XmlData类和XQuery支持。

引言

除非我在编写事务性业务逻辑层时使用COM+,否则我无法有效编写封装的代码。这意味着我不能让两个方法(属于不同组件中不同类)参与同一个声明式事务,而无需非常了解该事务(无论是通过显式共享同一个连接还是通过将它们的功能合并到一个公共存储过程中等等)。这意味着,我不能编写一个简单的Customer类,以及一个Order类,其中的逻辑完全封装在其中,并且可以在不修改、不合并逻辑等情况下在不同情况/应用程序中重用...

COM+是VS6世界中解决该问题的方法,但它有以下缺点:

  • 上下文拦截和两阶段提交的开销太大,它主要针对协调多个数据库的事务跨越多台计算机的场景。
  • COM+允许用特定的事务属性(例如RequiresNew)标记整个类,这使得在一个类中无法实现一个方法在没有事务的情况下运行,而另一个方法需要事务。这意味着,为了分离这些方法,开发人员必须创建两个类——例如EmployeeReader和EmployeeWriter,这很麻烦。
  • 默认情况下,COM+中的事务隔离级别是可序列化的,如果您想覆盖此设置,您必须在所有存储过程的开头使用SET TRANSACTION ISOLATION LEVEL ...。COM+的最新版本允许在管理级别设置隔离级别,但同样是在类级别。如果我希望在一个类中用不同的隔离设置标记我的方法呢?
  • 在.NET托管世界中使用COM+是完全可能的,但考虑到COM Interop等的额外开销,我发现它并不真正适用;)。许多人期待某种托管MTS...

另一个问题是,我希望集中我的数据访问代码(不喜欢编写大量相同的代码),并且能够之后轻松更改数据源(选择不同的数据库,甚至XML文件作为数据源),而无需重写应用程序的很大一部分。

我编写的数据访问组件(DAC)支持以下功能:

  • 方法级别的声明式事务——在一个类中,某些方法被标记为事务性并在特定的事务上下文中执行,而其他方法则在没有事务的情况下执行。例如,如果您有一个Employee类,其中包含GetInfo()Insert()方法,第一个方法将不参与事务(因为它只是选择数据),而第二个方法可以标记为事务性。
  • 事务可以跨越多个数据源。如果一个数据源中的操作失败,其他数据源将自动回滚。
  • 支持XML文件作为数据源,并支持XQuery语言进行查询.
  • 所有数据类的统一设计允许插入新类的通用接口底层实现的抽象
  • 无事务数据访问——如果方法未指定事务属性,则DAC将短路并跳过事务内部代码 => 几乎没有额外开销(与直接使用ADO.NET特定类相比)。

使用DAC,我能够完成以下(否则不可能的)任务:

  • 对不同的数据源、数据库等使用完全相同的接口。如果需要,稍后可以从一个切换到另一个。
  • 访问数据存储时编写更少的代码。
  • 在方法级别指定事务,其功能与COM+类似。
  • 跨两个或更多不同数据源的事务。
  • 当我不想使用事务时,几乎没有额外开销;当我使用事务时,额外开销最小化。

总而言之,与COM+提供的事务支持相比,DAC提供了扩展功能,且开销更小。缺点是它会保持连接打开稍长一点,但仅在执行线程中的方法被标记为事务性时。这就是生活,没有什么是免费的。;) 如果使用得当,我认为DAC是目前最好的权衡。

客户端视角

让我们从一个使用DAC的业务逻辑层类的事务性方法的简单示例开始

[TransactionContext(Transaction.Required, Isolation = Isolation.ReadUncommitted)]
public void Method1() {
    try {
        IData data = (IData)(new MsSqlData(_connectionString));
        DataParamCol dataParams = new DataParamCol(
            new InputDataParam("@Description", "TestTransactions.Method1")
            );
        data.Modify("Table4_Insert", dataParams);
        data = null;

        TransactionManager.SetComplete();
    }
    catch(Exception e) {
        TransactionManager.SetAbort();
        Console.WriteLine(e.Message);
    }
}

Method1() 简单地执行一个存储过程,该过程将一个值插入到 Table4 中,并将该值作为输入参数传递。

首先要注意的是 TransactionContext 属性。其中使用了两个枚举——Transaction 和 Isolation。

public enum Transaction { 
    Disabled = 0,// no transaction context will be created
    NotSupported,// transaction context will be created 
            // managing internally a connection, no transaction is opened though
    Required,// transaction context will be created if not present 
            // managing internally a connection and a transaction
    RequiresNew,// a new transaction context will be created 
            // managing internally a connection and a transaction
    Supported// an existing appropriate transaction context will be joined if present
}
    
public enum Isolation {
    Chaos = 0,        // not really used, copied from Data.IsolationLevel
    ReadCommitted,    
    ReadUncommitted,
    RepeatableRead,
    Serializable,
    Unspecified        // default, meaning not set
}

在执行数据操作时会查找TransactionContextAttribute。在幕后会创建一个事务上下文,它在内部管理连接,并在某些情况下管理事务。

如果使用Transaction.DisabledTransaction.NotSupported,则无需指定隔离属性,因为连接上不会打开任何事务。Transaction.Disabled等同于跳过TransactionContext属性。如果未指定TransactionContext属性(或指定Transaction.Disabled),则在当前方法结束时无需调用TransactionManager.SetCompleteTransactionManager.SetAbort。在这种情况下,Connection对象在数据操作之前打开,并在执行后立即关闭。如果使用Transaction.SupportedTransaction.RequiredTransaction.RequiresNew,则会考虑隔离属性,Connection和Transaction ADO.NET对象在内部管理,并且在方法结束时必须调用TransactionManager.SetCompleteTransactionManager.SetAbort,否则将留下一个打开的Connection。

接下来是实例化一个特定的数据源类。MsSqlData(连接到MS SQL Server)、OleDbData(连接到其他数据源)和XmlData(XML文件作为数据源)类被实例化并向上转型为通用接口IData

public interface IData {
    //---------------------------select, output parameters----------------------------
    #region Description
    /// <summary>
    /// Gets data ONLY in OUTPUT parameters. 
    /// Should be the fastest way.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="dataParams">stored procedure parameters</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, DataParamCol dataParams);

    //-----------------------------select, Untyped DataSet-----------------------------
    #region Description
    /// <summary>
    /// Creates and populates a DataSet with data. 
    /// The stored procedure does not accept parameters.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="ds">Reference to a DataSet. Should not be instantiated.</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, out DataSet ds);
    #region Description
    /// <summary>
    /// Creates and populates a DataSet with data. 
    /// The stored procedure does not accept parameters.
    /// The tables names in the DataSet are specified.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="ds">Reference to a DataSet. Should not be instantiated.</param>
    /// <param name="tableNames">Array of table names to be created inside the 
    /// DataSet</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, out DataSet ds, string[] tableNames);
    #region Description
    /// <summary>
    /// Creates and populates a DataSet with data. 
    /// The stored procedure accepts parameters.
    /// Default table names in the DataSet (Table, Table1, Table2....)
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="dataParams">stored procedure parameters</param>
    /// <param name="ds">Reference to a DataSet. Should not be instantiated.</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, DataParamCol dataParams, out DataSet ds);
    #region Description
    /// <summary>
    /// Creates and populates a DataSet with data. 
    /// The stored procedure accepts parameters.
    /// The tables names in the DataSet are specified.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="dataParams">stored procedure parameters</param>
    /// <param name="ds">Reference to a DataSet. Should not be instantiated.</param>
    /// <param name="tableNames">Array of table names to be created inside the 
    /// DataSet</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, DataParamCol dataParams, out DataSet ds, 
                 string[] tableNames);

    //--------------------------select, Typed DataSet--------------------------
    #region Description
    /// <summary>
    /// Populates a Strongly Typed DataSet with data.
    /// The stored procedure does not accept parameters.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="ds">Already instantiated Strongly Typed DataSet</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, DataSet ds);
    #region Description
    /// <summary>
    /// Populates a Strongly Typed DataSet with data.
    /// The stored procedure accepts parameters.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="dataParams">stored procedure parameters</param>
    /// <param name="ds">Already instantiated Strongly Typed DataSet</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, DataParamCol dataParams, DataSet ds);

    //--------------------------select, IDataReader---------------------------
    #region Description
    /// <summary>
    /// Creates and Loads an IDataReader with data.
    /// The stored procedure does not accept parameters.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="dr">Reference to an IDataReader. Should not be 
    /// instantiated.</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, out IDataReader dr);
    #region Description
    /// <summary>
    /// Creates and Loads an IDataReader with data.
    /// The stored procedure accepts parameters.
    /// </summary>
    /// <param name="commandText">stored procedure for SELECT</param>
    /// <param name="dataParams">stored procedure parameters</param>
    /// <param name="dr">Reference to an IDataReader. Should not be 
    /// instantiated.</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Retrieve(string commandText, DataParamCol dataParams, 
                 out IDataReader dr);  //input parameters

    //-----------------------------insert/update/delete---------------------------
    #region Description
    /// <summary>
    /// Modifies the Data Store 
    /// by simply executing an INSERT || UPDATE || DELETE stored procedure
    /// </summary>
    /// <param name="commandText">stored procedure for INSERT || UPDATE || 
    /// DELETE</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Modify(string commandText);
    #region Description
    /// <summary>
    /// Modifies the Data Store 
    /// by executing an INSERT || UPDATE || DELETE stored procedure with 
    /// parameters
    /// </summary>
    /// <param name="commandText">stored procedure for INSERT || UPDATE || 
    /// DELETE</param>
    /// <param name="dataParams">stored procedure parameters</param>
    /// <returns>RecordsAffected</returns>
    #endregion
    int Modify(string commandText, DataParamCol dataParams);

    //-----------insert/update/delete DataSet using a DataAdapter---------------
    #region Description
    /// <summary>
    /// Modifies the Data Store 
    /// by using a DataAdapter on the DataSet
    /// Single DataAdapterAction allowed
    /// </summary>
    /// <param name="dataAdapterCommand">the custom DataAdapterCommand</param>
    /// <param name="ds">DataSet, containing Data Store changes</param>
    /// <param name="tableName">specific Table in the DataSet</param>
    #endregion
    void Modify(
        DataAdapterCommand dataAdapterCommand, 
        DataSet ds, 
        string tableName);
    #region Description
    /// <summary>
    /// Modifies the Data Store 
    /// by using a DataAdapter on the DataSet
    /// Multiple DataActions allowed
    /// </summary>
    /// <param name="dataAdapterCommands">Array of custom 
    /// DataAdapterCommands</param>
    /// <param name="ds">DataSet, containing Data Store changes</param>
    /// <param name="tableName">specific Table in the DataSet</param>
    #endregion
    void Modify(
        DataAdapterCommand[] dataAdapterCommands, 
        DataSet ds, 
        string tableName);
}

因此,以下实例化是可能的:

IData data = (IData)(new MsSqlData("Server=WIETEC29;Database=Test;" + 
                                   "User ID=sa;Password= ")); 
IData data = (IData)(new OleDbData(@"Provider= Microsoft.Jet.OLEDB.4.0;" + 
    @"Data Source= C:\TryProjects\DAL3\DB\db1.mdb;" + 
    @"User Id=admin;Password=;")); 

等等。

之后,实例化一个DataParamCol(如果需要),它是一个DataParam对象的集合。后者可以在构造函数中或稍后实例化并添加到集合中。

DataParamCol dataParams = new DataParamCol(
            new InputDataParam("@Description", "SomeStringValue"),
            new OutputDataParam("@Param2", SqlDbType.Int),
            new ReturnDataParam("@RETURN_STATUS")
                                              );
DataParamCol dataParams = new DataParamCol();
dataParams.Add(new InputDataParam("@Description", "SomeStringValue"));

下一步是对数据源执行命令,例如

data.Modify("Table4_Insert", dataParams);

数据操作通常可以是检索(有许多重载——返回输出参数、DataSets、IDataReaders)或修改(简单的INSERT/UPDATE/DELETE命令或DataSet/DataAdapter INSERT/UPDATE/DELETE)。

最后一步是对事务进行投票

TransactionManager.SetComplete();

TransactionManager.SetAbort();

在某些情况下,当您不想关闭连接时(例如返回 IDataReader 时),会使用TransactionManager.SetOnHold();TransactionManager.SetHoldComplete();

服务器实现

现在我们来看看服务器实现的细节。

Sample Image

有三个主要的客户端类——MsSqlDataOleDbDataXmlData,它们都实现了IData接口。前两个继承自公共抽象类DbData,该类实现了IData。当有适用于Oracle数据库的托管提供程序时,可以优雅地将其他类(如OraData等)添加到框架中。这种设计允许以相同的方式处理不同的数据源(通过在创建时将其向上转换为IData),并最终实现轻松的数据源替换。抽象类DbData包含MsSqlDataOleDbData的公共实现。实际上,后两个类的目的是“生成”数据提供程序特定的ADO.NET类。

所有数据库操作都旨在仅执行存储过程。使用存储过程与内联 SQL 语句相比具有众多优点,本文将不予讨论。

Sample Image

DataParam类是不同参数类的基类——InputDataParamBoundInputDataparamOutputDataParamReturnDataParamDataParamCol类的构造函数接受可变数量的基类DataParam对象。每个特定的DataParam类都有几个重载的构造函数。

DataParamCol类表示数据参数的集合。允许通过索引和键访问参数。此外,DataParamCol类还具有用于单独添加DataParam对象的Add()方法和Clear()方法。

自动事务通过TransactionManager class支持,该类只有静态方法。除了公共的SetComplete()SetAbort()SetOnHold()SetHoldComplete()之外,它还有用于定位当前事务上下文、创建新事务上下文等的内部方法。对于每个单独的事务,都会创建一个新的TransactionContext,其中包含连接和事务对象,并在内部管理它们的生命周期。

自动事务的核心是.NET Framework的CallContext类,它类似于线程局部存储,包含特定于当前运行线程的数据,并可供线程调用堆栈中的所有方法使用。TransactionContextsCallContext中创建;它们被方法使用,并在调用堆栈中的最后一个方法完成其数据库操作后销毁。TransactionContextCol类是CallContext的一个精简包装器,处理存储在CallContext中的TransactionContext对象集合。

internal class TransactionContextCol {
    public static void AddContext(TransactionContext TC) {
        CallContext.SetData(TC.Name, TC);
    }
    public static TransactionContext GetContext(string tcName) {
        return(CallContext.GetData(tcName) as TransactionContext);
    }
    public static void RemoveContext(string tcName) {
        CallContext.FreeNamedDataSlot(tcName);
    }
}

当执行一个具有事务/隔离属性唯一组合的方法时,会在线程的CallContext中创建一个新的TransactionContext。如果此方法执行另一个具有相同属性的方法,则第二个方法在相同的事务上下文中执行,否则会创建另一个事务上下文。当执行到达SetComplete()SetAbort()时,它会检查调用堆栈的深度,如果它等于创建事务上下文的方法的深度,它会进一步检查Happy标志并相应地提交或回滚事务。如果方法未标记事务属性,则应用默认值(TransactionEnum.DisabledIsolationEnum.Unspecified)。此外,方法应在其返回之前发出TrCtx.SetComplete()TrCtx.SetAbort()。这类似于COM+,您应该调用GetObjectContext.SetComplete()GetObjectContext.SetAbort()TrCtx类的这两个静态方法触发事务上下文处理过程——首先检查这是否是事务上下文中的ROOT方法,如果是,则确定是提交还是回滚整个事务。

运行示例项目

为了成功编译和运行示例项目,您应该提供以下内容:

  1. 确保您已安装.NET RTM ;)。
  2. 更改数据库连接字符串并执行项目存档中的SQL脚本,或者创建您自己的存储过程/表。
  3. 编译项目并运行DACClient示例控制台应用程序...

本文的第二部分将重点介绍XmlData类。

我不是一个经验丰富的C#程序员,因此非常感谢您的评论和建议。

历史

2002年10月30日 - 更新了演示。

© . All rights reserved.