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

数据访问和事务处理框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (97投票s)

2003年4月9日

10分钟阅读

viewsIcon

978970

downloadIcon

2133

用于不同数据源、存储过程/SQL、隐式传播的事务、显式管理的事务上下文等的通用数据访问组件。

引言

这是关于多层业务应用程序(例如 Web 应用程序)中数据访问和事务处理问题的又一次尝试。建议的方法应该有助于构建与数据库无关的数据层。通过简单地将 XML CONFIG 文件转换为所需的数据库方言,以后可以更改数据库。SQL 和存储过程命令都可以使用,并且可以轻松切换,而无需更改数据层代码。事务是隐式管理的,但代码显式处理事务上下文。提供了几个 TransactionHandlers。

背景

多层应用程序中的数据层取决于主要的应用程序架构。文章其余部分所依据的架构设计包括外观层、业务逻辑层、数据层,当然还有前面的 UI 和后面的数据库。中间层作为一个整体是无状态的(状态保存在 DB 中),数据应该双向通过它——从 UI 到数据库,从数据库到 UI。该架构是一个长篇故事,我将不详述其细节。事务可以从不同的层控制,通常是从外观层或业务逻辑层,而数据访问代码通常放在数据层。

目标

  1. 与数据库无关的数据层 - 这意味着只需重写包含命令定义的 XML 文件即可更改数据库,而无需重写数据层代码(类似于本地化)。
  2. 支持不同的数据提供程序和多个数据源 - 可以通过指定其名称从工厂检索某个数据源。
  3. 隐式支持存储过程以及 SQL 语句和 SQL 批处理 - 最初可以在 CONFIG 文件中指定并保存 SQL 批处理,之后可以将其更改为数据源中的存储过程,而不会影响数据层代码。
  4. 命令参数被缓存,并且只在代码中设置值(无需在代码中定义参数)。
  5. 支持 DataSet/DataAdapter,通过 XML 或代码定义 DataAdapter,使用 CONFIG 文件中定义的命令。
  6. 隐式处理事务/连接,代码显式进入/提交/回滚/退出事务上下文。一个事务上下文可以跨越多个方法。一个方法可以跨越多个数据源进入多个嵌套事务上下文。

设计思路

  1. 包含可用数据提供程序和数据源的 CONFIG XML 文件。
  2. 每个数据源的 CONFIG XML 文件(在一个指定目录中,带有指定文件掩码,一个或多个),包含命令的定义。命令定义由 commandtextcommandtype 和参数组成。命令的名称可能与存储过程的名称相同,也可能不同。
  3. 如果 CONFIG 文件中未指定相应的命令,则可以直接从数据库派生存储过程。
  4. 当前的事务/连接存储在线程本地存储 (TLS) 中。一旦事务(+连接)打开,数据层就会隐式使用它。

使用 Framework.DataAccess 代码

IDataSource 用作 IDataCommands 的工厂。它代表一个特定的数据库和一个 .NET 数据提供程序,并缓存该数据库的所有命令。数据源定义如下:

<dataAccessSettings xmlns="Framework.DataAccess">
    <dataProviders>
        <dataProvider name="SqlClient" 
           connectionType="System.Data.SqlClient.SqlConnection, 
              System.Data, Version=1.0.3300.0, Culture=neutral, 
              PublicKeyToken=b77a5c561934e089"
           commandType="System.Data.SqlClient.SqlCommand, 
              System.Data, Version=1.0.3300.0, Culture=neutral, 
              PublicKeyToken=b77a5c561934e089"
           parameterType="System.Data.SqlClient.SqlParameter, 
              System.Data, Version=1.0.3300.0, Culture=neutral, 
              PublicKeyToken=b77a5c561934e089"
           parameterDbType="System.Data.SqlDbType, System.Data, 
              Version=1.0.3300.0, Culture=neutral, 
              PublicKeyToken=b77a5c561934e089"
           parameterDbTypeProperty="SqlDbType"
           dataAdapterType="System.Data.SqlClient.SqlDataAdapter, 
              System.Data, Version=1.0.3300.0, Culture=neutral, 
              PublicKeyToken=b77a5c561934e089"
           commandBuilderType="System.Data.SqlClient.SqlCommandBuilder, 
              System.Data, Version=1.0.3300.0, Culture=neutral, 
              PublicKeyToken=b77a5c561934e089"
           parameterNamePrefix="@"/>
                   ...
    </dataProviders>
    <dataSources>
        <dataSource name="DataSource1" isDefault="true"
           provider="SqlClient"
           connectionString="Server=XXXXX;Database=XXXXXXXX;User 
              ID=XXXX;Password=XXXXXXXX"
           dataOperationsPath=
              "..\..\config\SqlClient.DataSource1.Commands*.config"/>
            .....
    </dataSources>
</dataAccessSettings>

这就是所有特定于提供程序的代码如何从代码中外部化并放入 CONFIG 文件中的方式。

实例化默认数据源需要以下操作:

IDataSource ds = DataSourceFactory.GetDataSource();

对于命名数据源:

IDataSource ds = DataSourceFactory.GetDataSource("DataSource1");

IDataCommand 是代表数据库操作的对象,该操作不产生输出、仅产生输出参数或产生 IDataReader

命令定义在 XML 文件中外部化,如下所示:

<?xml version="1.0" encoding="utf-8" ?> 
<dataOperations dataSource="DataSource1" 
     xmlns="Framework.DataAccess">
<dataCommands>
    <dataCommand name="Command1" type="Text">
        <commandText>INSERT INTO Users(Username) 
            VALUES('user')</commandText>
    </dataCommand>
</dataCommands>
....
</dataOperations>

带有两个参数且不返回任何内容的命令可以这样执行:

IDataSource ds = DataSourceFactory.GetDataSource();
IDataCommand cmd = ds.GetCommand("UpdateUserAmount");
cmd.Parameters["UserID"].Value = userID;
cmd.Parameters["Amount"].Value = amount;
cmd.ExecuteNonQuery();

执行返回 IDataReader 的命令如下所示:

IDataSource ds = DataSourceFactory.GetDataSource();
IDataCommand cmd = ds.GetCommand("ListUsers");
IDataReader dr = cmd.ExecuteReader();
ArrayList userInfos = new ArrayList();
while(dr.Read())
{
   userInfos.Add(new SampleState.UserInfo(dr.GetInt32(0)));
}
dr.Close();

使用 IDataCommand.Parameters[...].Value 检索输出参数。

IDataSource ds = DataSourceFactory.GetDataSource();
IDataCommand cmd = ds.GetCommand("GetUserDetails");
cmd.Parameters["UserID"].Value = userID;
cmd.ExecuteNonQuery();
byte userAge = (byte)cmd.Parameters["UserAge"].Value);

除了 IDataSource.GetCommand 方法,还可以即时创建命令,这些命令不会被缓存,如下所示:

IDataSource ds = DataSourceFactory.GetDataSource();    

IDataCommand cmd = ds.CreateCommand("InsertUser", 
    "InsertUser", CommandType.StoredProcedure);    
cmd.Parameters.Add("Username", "@Username", 
    SqlDbType.NVarChar, 50, ParameterDirection.Input, 
    "user1");    
cmd.Parameters.Add("UserID", "@UserID", DbType.Int32, 
    ParameterDirection.Output);    

int recordsAffected = cmd.ExecuteNonQuery();

int userID = (int)cmd.Parameters["UserID"].Value;

然而,这段代码会引入对特定数据提供程序的依赖,如果使用了数据提供程序 XXXDbType enum(这与通用 DbType 一样支持),以及对特定命令的依赖,之后您将无法更改 CommandType/CommandText 等,而不修改代码。

此外,命令可以直接从数据库派生。在这种情况下,命令名称用于查找存储过程。所有参数名称都可以通过跳过 Framework.DataAccess.dll.config XML 文件中定义的(例如 "@")前缀来访问。按名称获取命令的搜索顺序是:首先是 CONFIG 文件(如果有),然后是数据库。如果在任一位置都找不到数据命令,则会抛出异常。

另一种选择是使用 DataSets 作为 DTOs,并使用 DataAdapters 来填充和更新 DataSets。在 DAC2 中处理 DataAdapters 有两种方式——要么在 XML CONFIG 文件中定义它们,要么在代码中以编程方式构建它们,这两种方式都类似于 DataCommands 的处理。有一个轻量级包装器,称为 IDataSetAdapter,围绕 IDbDataAdapter,其 FillUpdate 方法考虑了 DAC2 框架。IDataSetAdapter 的 XML 定义如下:

<dataOperations dataSource="DataSource1" 
           xmlns="Framework.DataAccess">
    <dataSetAdapters>    
      <dataSetAdapter name="Adapter1" populateCommands="true">
        <selectCommand>gt;
          <dataCommand name="Adapter1_SelectCommand" type="Text">
            <commandText>SELECT * FROM Users WHERE Username 
                         = @Username</commandText>
              <parameters>
                <param key="Username" name="@Username" 
                    type="NVarChar" size="50" 
                    direction="Input" />
              </parameters>
          </dataCommand>
        </selectCommand>
        <tableMappings>
          <tableMapping sourceTable="Table" 
                     dataSetTable="Users"/>
        </tableMappings>
      </dataSetAdapter>   
    </dataSetAdapters>    
</dataOperations>

您可以使用上面定义的 IDataSetAdapter,如下所示:

IDataSource ds = DataSourceFactory.GetDataSource();
IDataSetAdapter ad = ds.GetDataSetAdapter("Adapter1");

ad.SelectCommand.Parameters["Username"].Value = "user1";

DataSet dataSet = new DataSet();
ad.Fill(dataSet);

相应地,您可以使用 IDataSetAdapter 进行更新:

IDataSource ds = DataSourceFactory.GetDataSource();
IDataSetAdapter ad = ds.GetDataSetAdapter("Adapter1");

ad.SelectCommand.Parameters["Username"].Value = "user1";

DataSet dataSet = new DataSet();
ad.Fill(dataSet);
dataSet.Tables[0].Rows[0][1] = "user1MODIFIED";
recordsAffected = ad.Update(dataSet);

通过设置 XML CONFIG 中的 populateCommands=true 属性,或在手动调用 IDataSetAdapter.PopulateCommands() 后,可以调用 CommandBuilder 生成 INSERT/UPDATE/DELETE 命令。

正如我已经提到的,IDataSetAdapter 可以在代码中以编程方式创建,而无需使用 XML CONFIG 文件。只有 IDataCommands 可以在“本地化”的 XML 文件中定义,而与数据提供程序无关的 IDataSetAdapter 可以在代码中实例化,并且其 Select/Insert/Update/DeleteCommand 属性可以设置为从 CONFIGs 检索的 IDataCommands

IDataSource ds = DataSourceFactory.GetDataSource();
IDataCommand cmd = ds.GetCommand("Command5");
cmd.Parameters["Username"].Value = "user1";

IDataSetAdapter ad = ds.CreateDataSetAdapter();
ad.SelectCommand = cmd;
ITableMapping dtm = ad.TableMappings.Add("st","dt");

ad.PopulateCommands();

DataSet dataSet = new DataSet();
int recordsAffected = ad.Fill(dataSet);

dataSet.Tables[0].Rows[0][1] = "user1MODIFIED";
recordsAffected = ad.Update(dataSet);

在 CONFIG 文件中定义 IDataSetAdapters 可以增加灵活性(如果需要),尽管它们的位置通常不应在那里,因为它们与数据提供程序无关(这是它们与 IDataCommands 的区别)。

关于 IDataSetAdapters 的最后一点是,它们填充/更新 DataSets。其他适配器可能填充/更新 Hashtables 甚至自定义业务对象(请参阅 IBatis db layer 中的 SQLMaps)。然而,问题在于,在 CONFIG 文件中定义自定义业务对象的检索/修改(以及增加的复杂性)是否真的有益...

Framework.DataAccess 组件应按以下方式使用。应在数据层类中检索 IDataSource(一个好的位置是在构造函数中并存储在私有变量中),并使用 IDataSource.GetCommand 方法从 IDataSource 检索 IDataCommand。这样,所有与数据库/提供程序/命令相关的代码都位于应用程序外部,并驻留在 CONFIG 文件中;因此可以轻松替换。每次修改 CONFIG 文件时,都会触发一个事件,DataSourceFactory 的内部缓存由某个 IDataSource 内部使用。DataCommandFactory 会使用最新的信息重新填充,而不会停止应用程序。

使用 Framework.Transactions 代码

Framework.Transactions.dll 是包中的另一个组件,与 Framework.DataAccess.dll 一起。事务上下文的构想全部归功于 David Goldstein,我想亲自感谢他与我分享!

TransactionContextFactory 是服务于新 TransactionContexts 请求的对象。一个类型为 TransactionAffinity 的参数被传递给它。TransactionAffinity enum 的值如下:

    public enum TransactionAffinity
    {
        //creates new transaction
        RequiresNew,
        //creates new transaction if no current transaction
        Required,
        //uses current transaction if present
        Supported,
        //does not use a transaction
        NotSupported
    }

TransactionAffinity 值与 COM+ 事务类型(也在 EnterpriseServices 中)非常匹配。因此,请求 TransactionContext 的方式如下:

TransactionContext ctx = 
   TransactionContextFactory.GetContext(
          TransactionAffinity.RequiresNew);

使用 TransactionContext 的标准结构如下:

    ctx.Enter();
    //call bll/dal methods(db operations)
    ctx.VoteCommit() or ctx.VoteRollback()
    ctx.Exit();

如果我们添加异常处理,它会变成这样:

    TransactionContext ctx = 
        TransactionContextFactory.GetContext(
               TransactionAffinity.RequiresNew);

    try 
    {
        ctx.Enter();

        //call bll/dal methods(db operations)

        ctx.VoteCommit();
    }
    catch(Exception e) 
    {
        ctx.VoteRollback();
    }
    finally 
    {
        ctx.Exit();
    }

还有一个简便的方法来管理 EnterExit

using(TransactionContext ctx = 
     TransactionContextFactory.EnterContext(
               TransactionAffinity.Supported)) 
    {
        //already entered

        ctx.VoteCommit() or exception for example
        //automatic exit upon leaving the scope ...
    }

这里的好处是,可以在任何逻辑应用程序层创建和进入 TransactionContext,而无需调用者了解。这是在任何级别显式声明代码需求的.*. 可以在方法范围内使用多个上下文,也可以使用一个上下文跨越多个方法。与 COM+ 中的现有限制.*. 没有限制!

TransactionContext 还有一个类型为 TransactionIsolationLevelIsolationLevel 属性,它决定了打开的事务的隔离级别。请记住,只有控制事务的上下文(即 Controlling Contexts - RequiresNew, Required)才会考虑此属性,其他上下文不关心其值。

    public enum TransactionIsolationLevel
    {
        ReadUncommitted,
        ReadCommitted,
        RepeatableRead,
        Serializable
    }

Framework.DataAcccess.dll 包含几种事务处理实现:

  • HomeGrownTransactionHandler(内部处理连接和事务),
  • SWCTransactionHandler(使用 COM+ 1.5 中的 Services Without Components)和
  • ESTransactionHandler(使用 ServicedComponent,而无需业务类继承自 ServicedComponent!)。

Framework.DataAccess.dll 中的以下部分确定了应该使用哪一个(一个开关):

[编辑说明:添加了换行符以避免滚动]

<transactionHandlingSettings xmlns="Framework.Transactions">
  <transactionHandler name="HomeGrown" 
    handlerType="Framework.DataAccess.TransactionHandling.
                                HomeGrownTransactionHandler, 
             Framework.DataAccess.TransactionHandling"
            />

</transactionHandlingSettings>

Framework.Transactions 组件应按以下方式使用。Facade 和 BLL 层应引用它,并使用 TransactionContextFactory 来实例化 TransactionContexts,并使用上述骨架代码来 Enter/VoteCommit/VoteRollback/Exit 上下文。Framework.DataAccess.dll 内部引用 Framework.Transactions.dll 并订阅事件,以便它可以管理与当前可用事务上下文对应的事务和连接。这就是事务从上层控制,而数据层拾取它们并在数据访问中使用它们的方式。

CommandText 运行时修改

您一定遇到过以下问题——有一个查询数据的要求(通常用于报告目的),其中包含许多可选过滤器。在这种情况下,如果您使用存储过程,则必须将过滤器添加为可选的 sproc 参数,这会导致非常糟糕的执行计划(至少在 SQL Server 中)。当然,答案是动态 SQL,它会省略没有提供值的可选参数。但是,应该如何构建这个动态 SQL?在代码内部?处理这种情况的一个可能方法是 replaceByParamValues 标签,可以在 dataCommand 定义中使用。这样,就可以指定在输入参数具有某个值时 CommandText 的哪些部分可以被省略,或者应该添加什么附加文本(通过替换某些占位符)。这是一个例子:

<dataCommand name="SelectUsersCommand2" type="Text">
   <commandText>
     <![CDATA[
      SELECT 
      COUNT(*)
      FROM Users AS U1
      WHERE 1=1
      AND U1.UserID < @UserID]]>
   </commandText>
   <replaceByParamValues>
     <replaceByParamValue paramName="@UserID" 
                      paramValue="DBNull.Value">
       <oldString><![CDATA[AND U1.UserID < @UserID]]>
       </oldString>
       <newString></newString>
     </replaceByParamValue>
   </replaceByParamValues>
   <parameters>
     <param name="@UserID" type="Int" direction="Input" />
   </parameters>   
</dataCommand>

历史

  • 13.01.2005
    • 代码更新。添加了命令超时,创建 DB 对象而不是克隆它们的可能性(如果提供程序不支持克隆),以及新的 replaceByParamValues 标签,允许在运行时自定义 CommandText
  • 23.09.2004
    • 重新组织了 CONFIG 文件(移至 App.config)。添加了 ESTransactionHandler(使用 COM+ 和 ServicedComponent,而无需将业务/服务类继承自 ServicedComponent)。在 DataSetAdapter 定义中添加了 ColumnMappings
  • 08.07.2004
    • 添加了直接从数据源派生 sproc 的可能性(如果 CONFIG 文件中找不到命令(或者没有 CONFIG 文件),则会查询数据库(使用 DeriveCommand))。
  • 14.04.2004
    • bug 修复(DataSetAdapter),没有新功能。
  • 20.10.2003
    • 下载更新(HomeGrown Transaction 实现 bug 修复,添加了 "parameterNamePrefix" 和 "key" CONFIG XML 属性,因此参数名称也与使用的具体 DB 隔离,Framework.Configuration 被分离等)。
  • 17.09.2003
    • 下载更新(SWC bug 修复,重新包含 installinstructions.htm)。
  • 11.09.2003
    • Services Without Components 事务处理实现(将在 Windows Server 2003 上测试)。XML CONFIG 架构更改(请查看代码下载)。CommitRollback 更改为 VoteCommitVoteRollbackExit() 方法现在完成事务。移除了 ITransactionContext 接口等。实现更改和修复。
  • 16.08.2003
    • 重构为两个组件(Framework.DataAccess.dllFramework.Transactions.dll)。TransactionContexts 实现。
  • 27.06.2003
    • 修复。
  • 10.06.2003
    • 添加了 DataSet/Adapter 支持;更改了操作参数的 API(cmd.Parameters[...].Value 而不是 cmd.Set/GetParameterValue);DataCommandFactory -> DataOperationFactory,以及 CONFIG 属性定义 commandDir -> dataOperationsDircommandFileMask -> dataOperationsFileMask;在参数定义中添加了 SourceColumnSourceVersion 属性;项目已转换为 VS.NET 2003,.NET Framework 1.1。
  • 10.04.2003
    • 初始功能原型版本,没有适当的异常和参数检查/错误处理。

示例安装说明

请阅读演示项目根目录中的 InstallInstructions.htm

请求

我将非常感谢您的意见/更正/建议。

问题

  • 有人能测试 SWCTransactionHandler 在 Windows Server 2003 上的实现,并告诉我它是否有效吗?;)

谢谢

我想感谢 Dan Fox 的 Data Factory 示例,它给了我一些想法,以及 David Goldstein,他提出了显式事务上下文的设计。

© . All rights reserved.