数据访问和事务处理框架






4.69/5 (97投票s)
2003年4月9日
10分钟阅读

978970

2133
用于不同数据源、存储过程/SQL、隐式传播的事务、显式管理的事务上下文等的通用数据访问组件。
引言
这是关于多层业务应用程序(例如 Web 应用程序)中数据访问和事务处理问题的又一次尝试。建议的方法应该有助于构建与数据库无关的数据层。通过简单地将 XML CONFIG 文件转换为所需的数据库方言,以后可以更改数据库。SQL 和存储过程命令都可以使用,并且可以轻松切换,而无需更改数据层代码。事务是隐式管理的,但代码显式处理事务上下文。提供了几个 TransactionHandlers。
背景
多层应用程序中的数据层取决于主要的应用程序架构。文章其余部分所依据的架构设计包括外观层、业务逻辑层、数据层,当然还有前面的 UI 和后面的数据库。中间层作为一个整体是无状态的(状态保存在 DB 中),数据应该双向通过它——从 UI 到数据库,从数据库到 UI。该架构是一个长篇故事,我将不详述其细节。事务可以从不同的层控制,通常是从外观层或业务逻辑层,而数据访问代码通常放在数据层。
目标
- 与数据库无关的数据层 - 这意味着只需重写包含命令定义的 XML 文件即可更改数据库,而无需重写数据层代码(类似于本地化)。
- 支持不同的数据提供程序和多个数据源 - 可以通过指定其名称从工厂检索某个数据源。
- 隐式支持存储过程以及 SQL 语句和 SQL 批处理 - 最初可以在 CONFIG 文件中指定并保存 SQL 批处理,之后可以将其更改为数据源中的存储过程,而不会影响数据层代码。
- 命令参数被缓存,并且只在代码中设置值(无需在代码中定义参数)。
- 支持
DataSet
/DataAdapter
,通过 XML 或代码定义DataAdapter
,使用 CONFIG 文件中定义的命令。 - 隐式处理事务/连接,代码显式进入/提交/回滚/退出事务上下文。一个事务上下文可以跨越多个方法。一个方法可以跨越多个数据源进入多个嵌套事务上下文。
设计思路
- 包含可用数据提供程序和数据源的 CONFIG XML 文件。
- 每个数据源的 CONFIG XML 文件(在一个指定目录中,带有指定文件掩码,一个或多个),包含命令的定义。命令定义由
commandtext
、commandtype
和参数组成。命令的名称可能与存储过程的名称相同,也可能不同。 - 如果 CONFIG 文件中未指定相应的命令,则可以直接从数据库派生存储过程。
- 当前的事务/连接存储在线程本地存储 (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 文件(如果有),然后是数据库。如果在任一位置都找不到数据命令,则会抛出异常。
另一种选择是使用 DataSet
s 作为 DTOs,并使用 DataAdapters
来填充和更新 DataSet
s。在 DAC2 中处理 DataAdapters
有两种方式——要么在 XML CONFIG 文件中定义它们,要么在代码中以编程方式构建它们,这两种方式都类似于 DataCommands 的处理。有一个轻量级包装器,称为 IDataSetAdapter
,围绕 IDbDataAdapter
,其 Fill
和 Update
方法考虑了 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
的最后一点是,它们填充/更新 DataSet
s。其他适配器可能填充/更新 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();
}
还有一个简便的方法来管理 Enter
和 Exit
:
using(TransactionContext ctx =
TransactionContextFactory.EnterContext(
TransactionAffinity.Supported))
{
//already entered
ctx.VoteCommit() or exception for example
//automatic exit upon leaving the scope ...
}
这里的好处是,可以在任何逻辑应用程序层创建和进入 TransactionContext
,而无需调用者了解。这是在任何级别显式声明代码需求的.*. 可以在方法范围内使用多个上下文,也可以使用一个上下文跨越多个方法。与 COM+ 中的现有限制.*. 没有限制!
TransactionContext
还有一个类型为 TransactionIsolationLevel
的 IsolationLevel
属性,它决定了打开的事务的隔离级别。请记住,只有控制事务的上下文(即 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
。
- 代码更新。添加了命令超时,创建 DB 对象而不是克隆它们的可能性(如果提供程序不支持克隆),以及新的
- 23.09.2004
- 重新组织了 CONFIG 文件(移至 App.config)。添加了
ESTransactionHandler
(使用 COM+ 和ServicedComponent
,而无需将业务/服务类继承自ServicedComponent
)。在DataSetAdapter
定义中添加了ColumnMappings
。
- 重新组织了 CONFIG 文件(移至 App.config)。添加了
- 08.07.2004
- 添加了直接从数据源派生 sproc 的可能性(如果 CONFIG 文件中找不到命令(或者没有 CONFIG 文件),则会查询数据库(使用
DeriveCommand
))。
- 添加了直接从数据源派生 sproc 的可能性(如果 CONFIG 文件中找不到命令(或者没有 CONFIG 文件),则会查询数据库(使用
- 14.04.2004
- bug 修复(
DataSetAdapter
),没有新功能。
- bug 修复(
- 20.10.2003
- 下载更新(HomeGrown Transaction 实现 bug 修复,添加了 "
parameterNamePrefix
" 和 "key
" CONFIG XML 属性,因此参数名称也与使用的具体 DB 隔离,Framework.Configuration
被分离等)。
- 下载更新(HomeGrown Transaction 实现 bug 修复,添加了 "
- 17.09.2003
- 下载更新(SWC bug 修复,重新包含 installinstructions.htm)。
- 11.09.2003
- Services Without Components 事务处理实现(将在 Windows Server 2003 上测试)。XML CONFIG 架构更改(请查看代码下载)。
Commit
和Rollback
更改为VoteCommit
和VoteRollback
,Exit()
方法现在完成事务。移除了ITransactionContext
接口等。实现更改和修复。
- Services Without Components 事务处理实现(将在 Windows Server 2003 上测试)。XML CONFIG 架构更改(请查看代码下载)。
- 16.08.2003
- 重构为两个组件(Framework.DataAccess.dll 和 Framework.Transactions.dll)。
TransactionContexts
实现。
- 重构为两个组件(Framework.DataAccess.dll 和 Framework.Transactions.dll)。
- 27.06.2003
- 修复。
- 10.06.2003
- 添加了 DataSet/Adapter 支持;更改了操作参数的 API(
cmd.Parameters[...].Value
而不是cmd.Set
/GetParameterValue
);DataCommandFactory
->DataOperationFactory
,以及 CONFIG 属性定义commandDir
->dataOperationsDir
,commandFileMask
->dataOperationsFileMask
;在参数定义中添加了SourceColumn
和SourceVersion
属性;项目已转换为 VS.NET 2003,.NET Framework 1.1。
- 添加了 DataSet/Adapter 支持;更改了操作参数的 API(
- 10.04.2003
- 初始功能原型版本,没有适当的异常和参数检查/错误处理。
示例安装说明
请阅读演示项目根目录中的 InstallInstructions.htm。
请求
我将非常感谢您的意见/更正/建议。
问题
- 有人能测试
SWCTransactionHandler
在 Windows Server 2003 上的实现,并告诉我它是否有效吗?;)
谢谢
我想感谢 Dan Fox 的 Data Factory 示例,它给了我一些想法,以及 David Goldstein,他提出了显式事务上下文的设计。