为 ASP.NET 应用程序设计和实现一个通用的数据访问层






4.63/5 (40投票s)
2003年2月4日
28分钟阅读

388607

3823
在本文中,我们将深入探讨 n 层架构的设计,重点关注数据访问层 (DAT)
摘要
在本文中,我们将深入探讨 n 层架构的设计,重点关注数据访问层 (DAT)(图 0)。我们的目标是为一个可扩展、可靠且可互操作的 Web 应用程序设计并实现一个合适的数据访问层 (DAT)。我已将本文分为两个主要部分。在第一部分中,我们将讨论架构目标、类型化 DataSet、事件日志和 HttpModules,这些内容有助于理解本文的第二部分。在第二部分中,我们将构建一个小型(但精良)的 n 层应用程序,重点关注数据访问层的设计和实现。你需要一个 SqlServer(数据库:Northwind)和 VS.net 才能跟上本文的进度。
目录
1. 设计数据访问层 (DAT) 的目标
1.1 架构目标
1.2 原型的安装
1.3 类型化 DataSet
1.4 事件日志
1.5 HttpModules
2.0 实现通用的数据访问层 (DAT)
2.1 为数据访问层实现一个超类。
2.2 评估业务层中 SqlDataReader 和 DataSet 对象的性能测试。
2.3 如何扩展此数据访问层 (DAT) 以访问多个数据库。
2.4 如何应用具有不同隔离级别的数据库事务?
2.5 如何插入和检索图像?
图 0 向我们展示了一个典型的 3 层应用程序,它被分解为三个主要的互斥层,即数据层 (DA)、中间层和表示层 (PT)。
如果你不熟悉 n 层架构,我推荐以下文章。
- http://www.c-sharpcorner.com/Tutorials/Building3TierAppPA.asp
- http://www.c-sharpcorner.com/Code/2002/June/AdoNetWinDNAPerspective.asp
1. 设计一个合适的数据访问层 (DAT)
1.0 架构目标
企业级应用程序的必要需求可以通过以下流行语来表达:
- 互操作性与可扩展性
- 可伸缩性与性能
- 可靠性与可管理性
- 安全
让我们仔细看看其中一些流行语,它们到底是什么意思?
互操作性处理的是系统在异构环境中与其他内部或外部系统通信的能力。这是一个非常重要的问题,因为我们需要信息来及时行动,以满足客户和业务合作伙伴的需求。为了实现这个重要目标,我们的系统必须能够提供和使用 Web 服务。
为了在业务中保持敏捷,有时你需要扩展你的应用程序。如果我们将一个系统与其他系统耦合得太紧密,那么它可能会引发“修改雪崩”(例如,你通过电子邮件将数据传递给一个对等应用程序,该邮件通过逗号分隔的列来表示数据表。收件人将打开电子邮件并解析它以获取信息。假设,如果我们想要添加或更改一列,那么我们也必须修改其他依赖的系统,因为列的顺序已经改变了)。为了避免“修改雪崩”,我们必须使用 Web 服务等技术(确切地说:XML 支持的技术)来松散地耦合系统。
可伸缩性是描述系统通过增加额外系统资源来提高性能能力的一种度量。一个系统应用以下策略来变得可伸缩。
- 它必须能够分配新资源来满足不断增长的用户请求。
- 它必须使用不会长时间占用资源的算法和数据结构。
可伸缩性是一个非常重要的问题,因为你可以放心地投资一个系统,相信该系统会随着业务的增长而增长。
软件系统的可靠性可以定义为系统无故障执行其任务的概率。为实现更高的可靠性,系统必须提供跟踪可能错误的实用工具。
图 1 显示了 Northwind 数据库的部分 ER 图
所以,我们现在有了宏伟的目标。为了实现这些目标,我们必须打下坚实的基础。我们的数据访问层 (DAT) 必须设计为完成以下任务。
- 它必须使我们能够与一组数据源交互,并协调它们之间的事务
- 它必须能够灵活地使用不同的数据访问技术(SqlDataAdapter 和 DataSet),因为这个问题关系到系统的可伸缩性和性能。
- 它必须支持表示层 (PT) 以显示一致的数据表。关系数据库中大多数相互关联的表都是根据主从(父子)关系建模的。在我们的数据库示例中(参见图 1),你可以找到一些这样的组合,它们是:Customers-Orders、Orders-Order Details。在表示层 (PT) 中,总是需要向下钻取主表以显示详细信息。图 2 显示了主从关系 Customer-Order。
- 它必须提供基础设施来跟踪和分析错误。
图 2 显示了演示 Web 应用程序 DAPrototype 的屏幕截图
1.2 原型的安装
在我们继续之前,我建议你安装示例应用程序 DAPrototype,以便可以时常参考。解决方案 DAPrototype.sln 包含以下类库(见图 3)。
程序集 |
描述 |
BLT |
业务逻辑层的程序集 |
DAT |
数据访问层的程序集 |
HttpTraceModule |
包含一个 HttpModule,用于跟踪表示层中的错误
|
TDS |
包含类型化的数据集,用作在各层之间传输数据的载体 |
图 3
请按照以下说明安装示例应用程序
- 下载文件 DAPrototype.zip
- 创建一个新的 ASP.NET C# Web 应用程序 DAPrototype,并用你下载的文件替换它。
-
打开并执行 SQL 脚本 DAPrototype/SqlScript/DAPrototype.sql,以便在 Northwind 数据库上创建存储过程,这些存储过程是我们数据层 (DT) 的一部分。
- 为了为表示层、业务层和数据访问层创建事件日志 (Windows),请单独打开 C# 项目 DAProtype/ELCreator/ELCreator.csproj(参见 1.4)。请在那里调整机器名称并执行它。出于安全原因,我们不能将此创建功能集成到我们的 Web 应用程序中。Windows 用户 ASPNET 没有足够的权限来创建 Windows 资源。
- 调整 DAPrototype/Web.config 文件中键的配置值,特别是 localConnection、daMachine、blMachine 和 plMachine 键(见图 4)。
<appSettings> <!-- Database Connection --> <add key="LocalConnection" value="server=localhost;database=Northwind;uid=sa;pwd=moses; pooling=true; Max Pool Size=100;"/> <!-- Event Log for Data Access Tier --> <addkey="daMachine"value="ABRAHAM"/> <addkey="daLog"value="DALog"/> <addkey="daSource"value="DASource"/> <!-- Event Log for Business Tier --> <addkey="blMachine"value="ABRAHAM"/> <addkey="blLog"value="BLLog"/> <addkey="blSource"value="BLSource"/> <!-- Event Log for Prsentation Tier --> <addkey="plMachine"value="ABRAHAM"/> <addkey="plLog"value="PLLog"/> <addkey="plSource"value="PLSource"/> <!--- application constants--> <addkey="consumeTaxRate"value="0.16"/> </appSettings>
图 4
- 将页面 DAPrototype/SerchCustomOrders.aspx 设置为起始页。
我希望现在你可以运行该应用程序了。
1.3 类型化 DataSet
DataSet 是一个内存数据库,依赖于关系数据模型。DataSet 天生就适合跨层通信。与所有关系数据模型一样,它有数据表、外键约束、表关系的触发器。DataSet 是自给自足的,因为它们必须独立于提供数据的数据源。此外,DataSet 还能够处理像 XML DOM 这样的分层数据结构,方法是将 XML 元素视为表,将 XML 属性视为表属性。让我们看一个例子(见图 5)。所呈现的 XML 文件列出了德国的一些州及其城市。
<?xmlversion=”1.0”encoding=”utf-8”?>
<Germany>
<StateName=”Bayern”>
<CityName=”Munic”/>
<CityName=”Nürenburg”/>
</State>
<StateName=”NRW”>
<CityName=”Düsseldorf”/>
<CityName=”Bonn”/>
</State>
</Germany>
XML 文件
对应 DataSet 的 XSD 文件
图 5
在这种情况下,名为“German”的 DataSet 需要两个表,State 和 City,以及一个数据关系来表示这个 XML 文档。我们不想在这里进一步深入,因为它超出了我们的重点范围。
让我们来看一个典型的数据访问场景。假设你使用 SqlDataAdapter.Fill
的一个重载方法从数据库中填充一个 DataSet
(非类型化的)。SqlDataAdapter.Fill
方法接受一个 DataSet
和一个 DataTable 的名称作为参数。只有当 DataTables
和 DataColumns
在 DataSet 中尚不存在时,才会在其中创建它们。在这种情况下,它将使用 SqlDataAdapter.Schema
方法从数据源检索所需的模式,之后将创建适合加载数据的 DataTables
和 DataColumns
。
DataSet
(非类型化)在运行时提供了很大的可塑性,但它们容易出错,因为开发人员在编码时必须预测表及其属性。在最坏的情况下,开发人员必须从业务层 (BL) 追溯到存储过程,才能找出 DataSet 的内容。此外,并非总是能够追溯。当你通过 Web 服务从第三方收到一个 DataSet 并且它没有很好的文档时,就会出现这种情况。大多数与 DataSet 相关的问题(例如类型不匹配错误)只能在运行时捕获。你可以使用类型化 DataSet 来消除这类问题。
类型化 DataSet 是 DataSet 类的子类,它内置了固化的(硬编码的)DataTables 和 DataRows,这些都是为表示某个特定的关系数据模型而指定的。打个比方,非类型化 DataSet 就像 T 恤衫,而类型化 DataSet 可以比作量身定做的西装。
与非类型化 DataSet 相比,类型化 DataSet 具有以下优点。
- 它易于读写源代码,因为硬编码的 DataTables 和 DataRows 在编码时就暴露了属性及其类型。如果使用非类型化 DataSet,则必须在 DataTable 和 DataRow 集合中显式搜索它们,并且必须知道它们的类型。
- 类型不匹配错误可以在编译时发现。
- 对类型化 DataSet 的 DataTables 和 DataRows 的访问速度稍快,因为访问是在编译时确定的,你不需要在运行时通过访问集合来搜索 DataTables 和 DataRows。
现在,我们想向你展示如何使用 Visual Studio 的智能感知来创建一个类型化的 DataSet。我们想创建一个类型化的 DataSet DSCustomer,在其中我们可以存储来自客户及其订单的数据。我们将使用存储过程 SPSelCustomers 和 SPSelOrders(参见图 6)从主从表 Customer 和 Orders 中获取数据。
CREATE PROCEDURE SPSelCustomers
(
@CustomerID varchar(5)
)
AS
SELECT
C.CustomerID,C.CompanyName,C.Address,C.City
FROM Customers C
WHERE C.CustomerID LIKE @CustomerID + '%'
GO
CREATE PROCEDURE SPSelOrders
(
@CustomerID nchar(5)
)
AS
SELECT O.OrderID,O.CustomerID, O.OrderDate
FROM Orders O
WHERE O.CustomerID = @CustomerID
ORDER BY O.OrderDate DESC
GO
图 6
为了创建类型化的 DataSet DSCustomer,我们必须首先设计一个蓝图。一个 XSD 模式可以达到这个目的(参见图 7)。按照以下步骤创建这样的模式。
(你可以参考文件 DAProtype/TDS/DSCustomer.xsd)
- 创建一个新的 DataSet 文件 DSCustomer.xsd。
(打开解决方案资源管理器 -> 在项目上右键单击 -> 选择
添加 -> 添加新项 -> 选择 DataSet 并在那里输入 DSCustomer.xsd)。
- 创建表示内置 DataTables 的 XML 元素。(启动服务器资源管理器并导航到
服务器 -> SqlServer -> Northwind -> 存储过程,然后将 SPSelCustomers 和 SPSelOrders 拖放到 DSCustomer.xsd 上)。
- 在表 SPSelCustomers 和 SPSelOrders 之间创建一个 DataRelation(主从关系)。
(在表 SpSelCustomers 上右键单击 -> 添加 -> 新关系。参见图 8)。
- 在不改变底层模式的情况下,为 DataTables 和 DataRows 提供友好的名称。
(点击 XML 视图,并在文件 DSOrders.xsd 上输入以下由下划线标记的语句
- <xs:schemaid=DSCustomer
xmlns
:
codegen
=urn:schemas-microsoft-com:xml-msprop.
- <xs:elementname=SPSelCustomers
codegen
:
typedName
=Customer
codegen
:
typedPlural
=Customers
> - <xs:elementname=SPSelOrderscodegen:typedName=Ordercodegen:typedPlural=Orders>
现在,我们已将表 SPSelOrders 和 SpSelOrders 命名为 Customers 和 Orders,它们的 DataRows 分别命名为 Customer 和 Order)
- <xs:schemaid=DSCustomer
- 生成 DSOrder 类(切换回 DataSet 视图并右键单击 -> 生成 DataSet)
注意:如果你想在 Web 服务中使用类型化 DataSet,那么请务必记住,用于创建友好名称的 XML 注解在 WSDL(Web 服务描述语言)中不起作用。
图 7 类型化 DataSet “DSCustomer” 的 XSD 模式
图 8
下面的代码片段演示了使用类型化 DataSet “DSCustomer” 编写和阅读源代码非常方便,因为它将其内置的 DataTable 和 DataRows 作为属性公开。
foreach(DSCustomer.Customer drCustomer in dsCustomer.Customers)
{
if( drCustomer.CustomerID ==ANTON)
{
DSCustomer.Order[] oField = drCustomer.GetOrders();
foreach(DSCustomer.Order drOrder in oField)
{
// Do something genius
//int j = drOrder.OrderID;
}
}
}
1.4 事件日志
事件日志是集中的 Windows 资源,用于记录重要的软件和硬件事件,对于确定错误源是必不可少的。你可以轻松地将远程计算机的事件日志连接到本地计算机上,因此事件日志适用于分析部署在多台机器上的 n 层应用程序。
你可以使用 EventLog 类(命名空间:System.Diagnostics
)来处理事件日志,它们允许你写入不同类型的条目(例如,信息、警告、错误等),这有助于我们表达所发生事件的严重性。以下代码片段展示了 EventLog 类的基础知识。
// Initialize the names for the logs and their sources
string strMachine = abraham;
string strLogBL = BLLog;
string strSourceBL = BLSource;
// Create EventLog for the Business Logic
if(!EventLog.SourceExists(strSourceBL,strMachine))
{
EventLog.CreateEventSource(strSourceBL,strLogBL,strMachine);
Console.WriteLine(New log and a source for BL is created);
}
else
{
Console.WriteLine( source for BL exists already);
// Delete EventLog.Delete(strLogBL,strMachine);
}
// create an instance of EventLog of BL and enter some test entries
EventLog elBL= new EventLog(strLogBL,strMachine,strSourceBL);
elBL.WriteEntry(Test Error,EventLogEntryType.Error);
elBL.WriteEntry(Test Inforamtion,EventLogEntryType.Information);
elBL.WriteEntry(Test Warning,EventLogEntryType.Warning);
当然,你可以使用 EventLog.Delete 方法删除一个现有的 EventLog。写入事件日志会消耗资源(CPU 时间和磁盘空间),因此建议只写入重要的事件。
1.5 HttpModules
HttpModule 是一个用于自定义 ASP.NET 应用程序的有用工具。换句话说,当 CLR 处理来自客户端的 Web 请求时,你可以使用 HttpModules 来影响 HTML 渲染过程的每个阶段。我们稍后会详细看到,ASP.NET 中的请求处理依赖于管道处理模型,该模型支持并行处理(Web 花园)并增强了 Web 应用程序的可扩展性和可管理性。
让我们 잠깐 看看我们可以在哪里应用 HttpModules。在商业应用中,总是有收集数据用于客户画像、产品营销、预测业务趋势等的需求。你可以优雅地使用 HttpModules 来收集数据,而不会弄乱应用程序代码。它也适用于解决安全问题(例如,防止 Web 攻击的模块、授权)。此外,HttpModules 非常灵活(可管理),因为你可以在 web.config 文件中打开或关闭 HttpModules。我们在示例应用程序中使用 HttpModules 来记录表示层中发生的错误。我们必须首先理解 ASP.NET 管道处理模型才能编写 HttpModules。
图 9 描绘了 ASP.NET 管道
让我们看看当服务器处理一个请求时会发生什么。
假设一个客户端为页面 Test.aspx 提交了一个请求。
步骤 1
IIS 将提取页面名称的后缀,在我们的具体示例中是“.aspx”,以便加载适当的 ISAPI 扩展。IIS 将在 metabase 中查找并为后缀“.aspx”加载扩展 aspnet_isapi.dll。(你可以通过编辑应用程序配置对话框来管理扩展加载过程(参见图 10)。你可以通过启动 Internet 服务并展开 -> 右键单击默认网站 -> 选择“属性”选项 -> 选择“主目录”选项卡 -> 按下“配置”按钮来打开此对话框)
图 10
之后,由 aspnet_isap.dll 提供的 HttpExtensionProc 函数将被调用;随后,客户端请求 Test.aspx 将被
移交给 aspnet_wp.exe 进程。这个进程作为公共语言运行时(CLR)的非托管平台,这就是为什么在调试 ASP.NET 应用程序时,你需要将 aspnet_wp.exe 进程添加到 VS 调试器中。
第二步
现在,我们的请求被传递给一个 System.Web.HttpRuntime 对象,它标志着处理管道的入口点和托管代码的边界。实际上,HttpRuntime 类负责处理请求,但它将传入的请求通过管道进行进一步处理。客户端请求将被包装到一个 HttpContext 类的实例中,并将其传递给一个合适的 HttpApplication 类的实例。ASP.Net 将每个虚拟目录视为一个单独的应用程序。HttpRuntime 对象将检查客户端请求,并使用 HttpApplicationFactory 类的一个实例来创建或找到一个合适的 HttpApplication 类的实例。
HttpContext 类在管道中充当传输介质;因此,它在管道的每个阶段都是可访问的。HttpContext 类拥有 HttpRequest、HttpResponse、HttpSessionState、HttpServerUtility 等类的实例作为其内在组件。这使得该对象在其沿着处理管道的旅程中能够自给自足,提供有关请求、用户、会话的信息,并加载生成的 HTML 代码。
步骤 3
HttpApplication 类负责管理请求执行过程的整个生命周期;因此,它会引发事件来标记管道阶段(例如,BeginRequest、PreRequestHandlerExcecute、PostReuestHandler、EndRequesthandler、Error 等)。你现在可以编写处理程序来订阅 HttpApplication 类的事件,以便自定义执行过程。实现它有两种主要方式。你可以直接使用继承自 HttpApplication 类的 Global 类(在 Global.asax.cs 文件中实现),但如果你在 Gloabal 实例中硬编码处理程序,那么它会降低应用程序的可管理性和灵活性。HttpModules 被设计用来以一种优雅的方式解决这个问题。用设计模式的语言来说,HttpModules 可以被看作是主题 HttpApplication 的观察者,因为它们被设计用来拦截 HttpApplication 类的事件。它们实现了 IHttpModule 接口,并编译成一个单独的程序集。我们稍后会通过一个具体的例子向你展示如何编写一个 HttpModule。让我们进入下一步。
注意
一个 HttpApplication 对象可以用来处理多个请求,但它一次只能处理一个请求,而且 ASP.Net 并不将 HttpApplication 对象视为单例,尤其是在 Web Garden 模式下。因此,这个类不适合持有处理整个应用程序的属性,比如当前用户数。
第 4 步。
现在,我们来到了请求处理的高潮部分。在这个阶段,服务器端控件的内容被转换为 HTML 代码并保存到 HttpContext 类的实例中,这个场景可以通过抽象工厂模式来描述。让我们看看这个过程是如何实现的。
首先,HttpApplication 类的实例会选择一个合适的 HttpHandlerFactory 对象,该对象实现了 IHttpHandlerFactory 接口。在我们的例子 Test.aspx 中,HttpApplication 对象会查看 machine.config 文件 ( C:\WINNT\Microsoft.NET\Framework\v1.0.3705\CONFIG) 的 <httpHandlers> 部分,并因为后缀是“.aspx”而选择 HttpHandlerFactory:System.Web.UI.PageHandlerFactory。
为了获得一个实现了 IHttpHandler 接口的合适的 System.Web.UI.Page 类实例,HttpApplication 对象将调用 PageHanderFactory:GetHandler 方法。HttpApplication 对象会首先在 web.config 文件的 <httpHandlers> 部分下查找请求的 URL Test.aspx,以确定是否指定了任何 HttpHandler(Page)。如果找到一个规范,它将创建指定的 HttpHander 并将其交给 HttpApplication 对象。否则,PageHandlerFactory 将假定存在一个名为 Test 的 HttpHandler(Page)并创建 Test 页面,然后将其返回给 HttpApplication 对象。
最后,HttpRuntime 对象将调用 IHttpHandler 方法。
ProcessRequest (HttpContext context),在我们的例子中是 Test.ProcessRequest
(HttpContext context),用于将服务器端控件的内容加载到 HttpContext 类的实例中,如果它们为请求后执行事件实现了任何处理程序,它将返回通过 HttpModules。
现在,我们来看看如何实现一个 HttpModule。正如我们之前提到的,HttpModules 实现了 IHttpModule 接口。HttpApplication 类(更确切地说:Global 类)的实例负责实例化 HttpModules。IHttpInterface 接口公开了两个契约方法。
interface IHttpModule
{
void Init(HttpApplication httpApplication);
void Dispose();
}
方法 “void Init” 通常用于订阅来自 HttpApplication 的事件,而方法 “void Dispose” 则是释放非托管资源的潜在候选者。
我们在我们的原型 DAPrototype 中使用一个 HttpModule HttpTraceModule(见图 11)来记录发生在表示层(PL)的错误,因此我们拦截 HttpApplication.EndRequest 事件,并使用 HttpContext.Error 属性检索表示层(PL)中的初始错误。要使用此功能,你必须使用 AddError(Exception e) 方法将抛出的异常添加到当前的 HttpContext 中。
using System;
using System.Web;
using System.Diagnostics;
using System.Configuration;
using System.Text;
namespace HttpTraceModule
{
///<summary>
/// It used to record errors on the presentation layer(PL)
///</summary>
publicclass HttpTraceModule:IHttpModule
{
///<summary>
/// Event Log for the Presentation Layer
///</summary>
EventLog elPL;
///<summary>
///
///</summary>
public HttpTraceModule()
{
// Initialize Trace Listener
string strLog = ConfigurationSettings.AppSettings["plLog"];
string strSource = ConfigurationSettings.AppSettings["plSource"];
string strMachine = ConfigurationSettings.AppSettings["plMachine"];
if(EventLog.SourceExists(strLog,strMachine))
{
elPL = new EventLog(strLog,strMachine,strSource);
}
}
///<summary>
/// interface contract method
///</summary>
///<param name="httpApplication"></param>
publicvoid Init(HttpApplication httpApplication)
{
// Subscribe HttpApplication events
httpApplication.BeginRequest += new EventHandler(Application_BeginRequest);
httpApplication.EndRequest += new EventHandler(Application_EndRequest);
}
///<summary>
/// interface contract method
///</summary>
publicvoid Dispose()
{
}
///<summary>
/// Writes errors on the log of the Presentation Layer
///</summary>
///<param name="oException"></param>
///<param name="oContext"></param>
privatevoid WriteError(HttpContext oContext,Exception oException)
{
StringBuilder oBuilder = new StringBuilder();
// Record the requested URL and the client
oBuilder.Append("An Error took place, while attempting to request the URL: ");
oBuilder.Append(oContext.Request.RawUrl);
oBuilder.Append("**");
oBuilder.Append("Client Address:");
oBuilder.Append(oContext.Request.UserHostAddress);
oBuilder.Append("**");
// Record the error message
if( oException != null)
{
oBuilder.Append( oException.Message);
}
if(elPL != null)
{
elPL.WriteEntry(oBuilder.ToString(),EventLogEntryType.Error);
}
}
///<summary>
/// Callback method for the event HttpApplication.EndRequest
/// It is used to record all errors on the log of the Presentation Layer
///</summary>
///<param name="source"></param>
///<param name="e"></param>
privatevoid Application_EndRequest(object source, EventArgs e)
{
HttpApplication hApplication = (HttpApplication) source;
HttpContext oContext = hApplication.Context;
Exception oException = oContext.Error;
if(oException != null)
{
this.WriteError(oContext,oException);
}
}
///<summary>
/// Callback method for the event HttpApplication.BeginRequest
///</summary>
///<param name="source"></param>
///<param name="e"></param>
privatevoid Application_BeginRequest(object source, EventArgs e)
{
HttpApplication hApplication = (HttpApplication) source;
HttpContext oContext = hApplication.Context;
}
}
}
图 11
我们已经将这个模块编译到程序集 HttpTraceModule.dll 中,并且
你必须在 web.config 文件中添加以下条目才能激活此模块。
<httpModules>
<!-- Module for Traceing -->
<add type=HttpTraceModule.HttpTraceModule,HttpTraceModule name=HttpTraceModule/>
</httpModules>
2.0 实现一个通用的数据访问层(DAT)
现在,我们将实现一个数据访问层,它能满足我们在 1.0 节中概述的所有期望。首先,我们将设计一个超类。
2.1 为数据访问层 (DAT) 实现一个超类
我们现在想为我们的数据访问层 (DAT) 设计一个超类,它能够从其派生类中接管常规的数据访问例程。我们必须首先分析各种数据访问上下文,这些上下文可以分类为
- 使用 DataSet 选择 DataRows
- 使用 SqlDataReader 检索数据
- 直接使用 SqlCommand 更新和删除表 (Excecute–NonQuery)
让我们逐一分析这些数据访问过程
使用 DataSet 选择 DataRows
步骤 1
为数据库连接和存储过程“strSP”创建数据库连接对象和 SqlCommand 对象。
SqlConnection dbConnection_L = new SqlConnection(strLocalServer);
SqlCommand dbCommand_L = new SqlCommand (strSP,dbConnection_L);
dbCommand_L.CommandType = CommandType.StoredProcedure;
第二步
创建一个 SqlDataAdapter 类的实例,并将其与 SqlCommand 对象 dbCommand_L 关联。
SqlDataAdapter dbAdapter_L = new SqlDataAdapter();
dbAdapter_L.SelectCommand = dbCommand_L;
步骤 3
将查询参数添加到 SqlCommand 对象
SqlParameter oParameterIn = new SqlParameter(“@ParaIn”,SqlDbType.Int,4);
oParameterIn.Direction = ParameterDirection.Input;
oParameterIn.Value = 7;
dbCommand_L.Parameters.Add(oParameterIn);
步骤 4
打开数据库连接:dbConnection_L.Open(); 我们这里不需要显式打开连接,因为 dbAdapter_L 对象将在第 5 步中隐式打开连接。
步骤 5
填充数据集
DataSet dsOut = new Dataset();
string strTable = Customer ;
dbAdapter_L.Fill(dsOut,strTable);
步骤 6
关闭数据库连接:dbConnection_L.Close();
使用 SqlDataReader 检索数据
这种类型的数据访问与前一种略有不同。我们必须省略第 2 步并修改第 5 步。
步骤 1
创建一个用于连接数据库的对象和一个用于存储过程“strSP”的 SqlCommand 实例。
SqlConnection dbConnection_L = new SqlConnection(strLocalServer);
SqlCommand dbCommand_L = new SqlCommand (strSP,dbConnection_L);
dbCommand_L.CommandType = CommandType.StoredProcedure;
第二步
我们这里不需要第 2 步。
步骤 3
将查询参数添加到 SqlCommand 对象
SqlParameter oParameterIn = new SqlParameter(“@ParaIn”,SqlDbType.Int,4);
oParameterIn.Direction = ParameterDirection.Input;
oParameterIn.Value = 7;
dbCommand_L.Parameters.Add(oParameterIn);
步骤 4
打开数据库连接:dbConnection_L.Open();
步骤 5
通过执行 SqlCommand 的 ExcecuteReader 方法接收 reader
SqlDataReader oReader = dbCommand_L.ExecuteReader(CommandBehavior.CloseConnection);
在 dbCommand_L.ExecuteReader 方法中使用了枚举器 “CommandBehavior .CloseConnection” 来在检索到 SqlDataReader 对象后自动关闭数据库连接。
步骤 6
因为枚举器 “CommandBehavior CloseConnection”,我们不需要第 6 步。
更新和删除表(执行非查询)
这种类型的数据访问与之前的数据访问过程略有不同。让我们一步一步地看这种类型。
步骤 1
创建一个到数据库的连接对象和一个 SqlCommand 类的实例
用于存储过程“strSP”。
SqlConnection dbConnection_L = new SqlConnection(strLocalServer);
SqlCommand dbCommand_L = new SqlCommand (strSP,dbConnection_L);
dbCommand_L.CommandType = CommandType.StoredProcedure;
第二步
我们这里不需要这一步,因为在这个过程中我们不需要任何 SqlDataAdapter 类的实例。
步骤 3
将输入参数和输出参数添加到 SqlCommand 对象
SqlParameter oParameterIn = new SqlParameter(“@ParaIn “,SqlDbType.Int,4);
oParameterIn.Direction = ParameterDirection.Input;
oParameterIn.Value = 7;
dbCommand_L.Parameters.Add(oParameterIn);
SqlParameter oParameterOut = new SqlParameter(“@ParaOut”,SqlDbType.Int,4);
oParameterOut.Direction = ParameterDirection.Output;
dbCommand_L.Parameters.Add(oParameterOut);
步骤 4
显式打开数据库连接:dbConnection_L.Open();
步骤 5
执行非查询并检索输出参数值
int nAffected = dbCommand_L.ExecuteNonQuery();
int nOut = (int) oParameterOut.Value;
步骤 6
关闭数据库连接:dbConnection_L.Close();
如你所见,我们这里有设计超类的理想条件。首先,我们必须
将重要的数据访问对象封装为基类 DABasis 中的受保护元素(请同时参考图 12 和文件 DAPrototype/DAT/DABaisis.cs 中的源代码)
// Objects for the local database access
protected SqlConnection dbConnection_L;
protected SqlCommand dbCommand_L;
protected SqlDataAdapter dbAdapter_L;
protected OleDbTransaction dbTransaction_L;
我们这里使用后缀“_L”,以表示这些对象处理的是本地 SQL 服务器。如果我们的 DAT 处理多个数据库,这种表示法将增强清晰度。
我们现在将在
DABasis.Perpair_L(string strSP) 方法。它将创建一个到本地数据库的连接和一个 SqlCommand 类的实例,用于一个名为 strSP 的特定存储过程。
///<summary>
/// This method initialize data access utilities for the local
/// Sqlserver this method must be always called at first
///</summary>
///<param name=strSP>name of the stored procedure</param>
protectedvirtualvoid Prepair_L(string strSP)
{
try
{
dbConnection_L = new SqlConnection(strLocalServer);
dbCommand_L = new SqlCommand (strSP,dbConnection_L);
dbCommand_L.CommandType = CommandType.StoredProcedure;
}
catch(Exception oException)
{
string strMessage = Occurred in Prepa_L() ;
ErrorLog(strMessage,oException);
}
}
图 12 显示了数据访问层(DAT)的类图
我们不在超类中实现第 2 步的功能,因为这一步对所有数据访问方法来说都不是通用的。
第 3 步的功能将在派生类中实现,但超类通过一个参数工厂来支持这一步。这个参数工厂是根据抽象工厂模式设计的,它将为我们提供几种类型的 SqlParameter,这些 SqlParameter 具有输入和输出方向的变化,用于设置和检索参数的值。此外,超类 DABasis 提供了一个方法 virtualvoid AddParameter_L(object oParameter) 来将 SqlParameter 类的实例添加到 dbCommand_L 对象中。这使得你的代码紧凑而整洁。
超类 DABasis 通过 void Open_L() 方法支持第 4 步以打开到本地服务器的数据库,而其补充的第 6 步则由 void Close_L() 方法覆盖。
第 5 步的功能在方法 void GetDataSet_L(DataSet dsOut,string strTable) 和 int ExcecuteNonQuery() 中实现。现在,是时候看看我们如何在不同的数据库访问上下文中应用这些方法了。
在第一个实际例子中,我们想使用 DataSet 对象来检索一个订单的订单详情。实现的流程可以通过以下步骤来概述。
- 编写一个存储过程“SpSelOrderDetail”来访问数据库中的数据(见图 13)。
- 为存储过程创建一个类型化的 DataSet "DSOrderDetail"(参见 §1.3 中的方法)
- 在数据访问层(DAT)中编写一个数据访问方法:void DAOrder.GetOrderDetail(int nOrderID,out DSOrderDetail dsOrderDetail)(见图 14)。
CREATE PROCEDURE SPSelOrderDetail
(
@OrderID int
)
AS
SELECT OD.ProductID, OD.UnitPrice,OD.Quantity, P.ProductName
FROM [Order Details] OD INNER JOIN Products P
ON OD.ProductID = P.ProductID
WHERE OD.OrderID =@OrderID
GO
图 13
///<summary>
/// Retrieves order details for an OrderID
///</summary>
///<param name=nOrderID>OrderID</param>
///<param name=dsOrderDetail>typed DataSet</param>
publicvoid GetOrderDetail(int nOrderID,out DSOrderDetail dsOrderDetail)
{
// Create the typed Dataset for output
dsOrderDetail = new DSOrderDetail();
try
{
// lock the intrinsic DataAcces utilities
Monitor.Enter(this);
// intialize DataAcces utilities for the local SqlServer
Prepair_L(“SPSelOrderDetail”);dbAdapter_L = new SqlDataAdapter();
dbAdapter_L.SelectCommand = dbCommand_L;
// Add query parameters to the command
SqlParameter pmOrderID = pmFactory_In.GetPMInt4(“@OrderID”);pmOrderID.Value = nOrderID;
AddParameter_L(pmOrderID);
string strTable = dsOrderDetail.OrderDetails.TableName;
// Retrive the typed DataSet
GetDataSet_L(dsOrderDetail,strTable);
}
catch(Exception oException)
{
string strError;
strError = An Error Occured in DAOrder:GetOrderDetailOrders;
this.ErrorLog(strError,oException);
}
finally
{
this.Close_L();
Monitor.Exit(this);
}
}
图 14
注意:为了保护内在的数据访问对象免受并发线程的影响,我们使用了 Monitor.Enter(this) 方法及其补充方法
在 publicvoid GetOrderDetail(int nOrderID,out DSOrderDetail dsOrderDetail) 方法中使用 Monitor.Exit(this)。此外,我们将始终在数据访问层 (DAT) 中使用 try-catch-finally 块,我们在 try 块中执行核心操作,在 catch 块中:我们将可能的错误写入事件日志,在 finally 块中:我们清理在 try 块中使用的资源;
在第二个实际例子中,我们想演示 int DABais.ExcecuteNonQuery 方法,该方法可用于直接更新和插入数据库。如果你使用 ASP.NET 实现表示层(PL),直接对数据库进行更新比使用 SqlDataAdapter.Update 方法更新 DataSet 对象要高效得多,因为你必须在页面往返期间将会话内存中的特定 DataSet 实例存储起来;因此,这将对可伸缩性和性能产生不利影响。在这个例子中,我们将更新 Order Details 表的 Quantity 属性(见图 15)。
CREATE PROCEDURE SPUpOrderDetail
(
@OrderID int,
@ProductID int,
@Quantity int
)
AS
UPDATE [Order Details]
SET Quantity = @Quantity
WHERE ProductID=@ProductID AND OrderID=@OrderId
图 15
///<summary>
/// Updates quantity of a product in an order deatail
///</summary>
///<param name=nOrderID>OrderID</param>
///<param name=nProductID>Product</param>
///<param name=nQuantity>Quantity</param>
///<returns> affected Rows</returns>
publicint UpdateOrderDetail(int nOrderID,int nProductID,int nQuantity)
{
int nAffected = 0;
try
{
// lock the intrinsic DataAcces utilities
Monitor.Enter(this);
// intialize DataAcces utilities for the local SqlServer
Prepair_L(“SPUpOrderDetail”);// Add parameters to the command
SqlParameter pmOrderID = pmFactory_In.GetPMInt4(“@OrderID”);pmOrderID.Value = nOrderID;
AddParameter_L(pmOrderID);
SqlParameter pmProductID = pmFactory_In.GetPMInt4(“@ProdID”);pmProductID.Value = nProductID;
AddParameter_L(pmProductID);
SqlParameter pmQuantity = pmFactory_In.GetPMInt4(“@Quantity”);pmQuantity.Value = nQuantity;
AddParameter_L(pmQuantity);
// Open the Connection and exequte
this.Open_L();
nAffected = this.ExcecuteNonQuery();
}
catch(Exception oException)
{
string strError;
strError = An Error Occured in DAOrder:UpdateOrderDetail;
this.ErrorLog(strError,oException);
}
finally
{
this.Close_L();
Monitor.Exit(this);
}
return nAffected;
}
图 16
存储过程“SPUpOrderDetail”将在数据层 (DT) 中执行更新任务。int DAOrder.UpdateOrderDetail 方法(参见图 16)将从业务逻辑层 (BLT) 接收值,并为它们创建参数,然后创建的参数将在继承的 DABais.AddParameter_L 方法的帮助下添加到 SqlCommand 对象中。之后,我们将使用 DABais.ExcecuteNonQuery() 方法直接更新数据库。
在第三个例子中,我们将使用 SqlDataReader 对象来获取数据,这是使用 DataSet 对象的替代方法。这个例子的功能与第一个例子相同。方法 public SqlDataReader DAOrder.GetOrderDetail(int nOrderID) 检索一个订单的订单详情,并使用数据层(DT)中的存储过程“SpSelOrderDetail”(图 13)。
///<summary>
/// retrieves order details for an OrderID
///</summary>
///<param name=nOrderID>OrderID</param>
///<returns>SqlDataReader</returns>
public SqlDataReader GetOrderDetail(int nOrderID)
{
SqlDataReader oReader = null;
try
{
// lock the intrinsic DataAcces utilities
Monitor.Enter(this);
// intialize DataAcces utilities for the local SqlServer
Prepair_L(“SPSelOrderDetail”);// Add parameters to the command
SqlParameter pmOrderID = pmFactory_In.GetPMInt4(“@OrderID”);pmOrderID.Value = nOrderID;
AddParameter_L(pmOrderID);
// Open the connection to the local server and Excecutethe command
this.Open_L();
oReader = dbCommand_L.ExecuteReader(CommandBehavior.CloseConnection);
}
catch(Exception oException)
{
this.Close_L();
Monitor.Exit(this);
string strError;
strError = An Error Occured in DAOrder:GetOrderDetailOrders(DR);
this.ErrorLog(strError,oException);
}
return oReader;
}
图 17
与 DataSet 不同,SqlDataReader 会保持数据库连接打开,并逐行将数据从数据库检索到应用程序中。之后,你必须显式关闭数据库连接。当你需要快速读取大量数据且不需要内存数据库时,可以使用 SqlDataReader。在多层应用程序中,我更喜欢使用 DataSet 而不是 SqlDataReader,因为在将检索到的数据在表示层 (PL) 中可视化之前,总是需要在中间层计算一些业务指标。
在这种情况下,SqlDataReader 并不比 DataSet 快。让我通过一个性能测试来证实前面的说法。
2.2 评估业务层中 SqlDataReader 和 DataSet 对象的性能测试。
在本实验中,我们将使用 SqlDataReader 和 DataSet 实现一个典型的商业应用程序数据检索场景,然后将两种实现置于相同的、明确定义的压力条件下。我们可以使用微软 Web 应用程序压力测试工具来模拟多用户请求,并测量诸如每秒点击数和请求数等指标。
我认为将以下数据访问场景视为
商业 Web 应用程序中的一般情况。我们想为一个订单显示一个发票列表。为了简单起见,我们将从计算中省略奖金、根据产品类别而定的消费税率等。我们将使用简化的计算公式来计算一个订单的发票列表。
其中
(见图 1 中的表 Order Details)
2.2.1 DataSet 实现
我们将使用 DAOrder.GetOrderDetail(int nOrderID,out DSOrderDetail dsOrderDetail) 方法(见图 14)从数据库中检索数据。
为了计算总和,我们将用一个十进制属性:Price 来修改 DSOrderDetail DataSet 中的 OrderDetail 表(参见源代码 TDS/DSOrderDetail.xsd)。
///<summary>
/// Retrieves order details for an OrderID
///</summary>
///<param name=nOrderID>OrderID</param>
///<param name=dsOrderDetail>typed DataSet represent OrderDetail</param>
///<param name=dTotal>Total amount of an order</param>
public void GetOrderDetail(int nOrderID,out DSOrderDetail dsOrderDetail, outdecimal dTotal)
{
dsOrderDetail = null;
dTotal = 0;
try
{
DAOrder daOrder = new DAOrder();
daOrder.GetOrderDetail(nOrderID,out dsOrderDetail);
// compute the total amount of order
dsOrderDetail.OrderDetails.Columns[Price].Expression = “Quantity*UnitPrice”;
object oSum = dsOrderDetail.OrderDetails.Compute(Sum(Price),Quantity>0);
decimal dSum = Convert.ToDecimal(oSum);
dTotal = dSum*dConsumeTaxRate;
}
catch(Exception oException)
{
string strMessage = Error in BLOrderDetail:GetOrderDetail;
ErrorLog(strMessage,oException);
}
}
图 18 (方法 BLOrderDetail.GetOrderDetail)
在 BLOrderDetail.GetOrderDetail 方法(图 18)中,我们首先通过数据访问层从数据库中检索类型化的 DataSet "DSOrderDetail",然后使用表达式“Quantity*UnitPrice”来计算 Price 列。现在,我们可以通过对 Price 列求和来确定发票列表的总计,并且
将此总和乘以因子 dConsumeTaxRate。然后我们将在 DAPrototype/DSTest.aspx 页面上展示我们的发票列表。
2.2.1 SqlDataReader 实现
我们将使用存储过程 SPSelOrderDetail(图 13)和 public SqlDataReader DAOrder.GetOrderDetail(int nOrderID) 方法(图 17)将必要的数据从数据库检索到业务逻辑层 (BLT)。正如你在 public SqlDataReader BLOrder.GetOrderDetail(int nOrderID,outdecimal dTotal) 方法(图 19)中看到的,我们必须从数据库中检索两次数据。我们使用第一次数据检索来计算业务指标“总计”,第二次数据检索将传递到表示层以显示发票列表。(DAPrototype/DRTest.aspx)
///<summary>
/// Retrieves order details for an OrderID
///</summary>
///<param name=nOrderID>Id of an Order </param>
///<param name=dTotal> Total amount of invoice list </param>
///<returns> SqlDataReader </returns>
public SqlDataReader GetOrderDetail(int nOrderID,outdecimal dTotal)
{
dTotal = 0;
SqlDataReader oReader = null;
try
{
DAOrder daOrder = new DAOrder();
// compute the total amount of order
oReader = daOrder.GetOrderDetail(nOrderID);
while(oReader.Read())
{
decimal dUnitPrice = Convert.ToDecimal(oReader[UnitPrice]);
int nQuantity = Convert.ToInt16(oReader[Quantity]);
dTotal += dUnitPrice * nQuantity;
}
dTotal*= dConsumeTaxRate;
// Retrive for the presentation layer
oReader = daOrder.GetOrderDetail(nOrderID);
}
catch(Exception oException)
{
string strMessage = Error in BLOrderDetail:GetOrderDetail(DR);
ErrorLog(strMessage,oException);
}
return oReader;
}
图 19
2.2.2
我们现在可以开始性能测试了。我正在用我的家用电脑进行这次测试。
它使用 Windows 2000 操作系统,拥有一个 800 MHz 的单处理器和 130 MB 内存。Sql Server 2000、.Net Framework 和 Web 压力测试工具都安装在同一台机器上。在测试期间,我们将保持请求压力水平恒定,即 200 个并发浏览器连接。Web 压力测试工具将为 Dataset 实现 (DAPrototype/DSTest.aspx) 和 SqlDataReader 实现 (DAPrototype/DRTest.aspx) 发送请求五分钟。以下是结果:
DataSet implemetation
Total number of requests: 42.093
Average requests per second: 140,31
Average database connection per second:31,90
SqlDataReader implemetation
Total number of requests: 35.663
Average requests per second: 118,88
Average database connection per second:173,27
结论
Dataset 实现不仅比 SqlDataReader 实现稍快,而且它能更有效地使用数据库连接。此外,与 DataReader 相比,DataSet 使我们能够更高效地实现业务逻辑。
现在,让我们以问答的形式讨论剩下的方面。
2.3 如何扩展此数据访问层 (DAT) 以访问多个数据库。
扩展它非常容易。比如说,你想要扩展 DAT,让它能与数据库 X 一起工作。一个扩展流程如下所示。
- 在超类 DABasis 中创建受保护的属性,以表示来自命名空间 System.Data.SqlClient 的数据访问类;
protected SqlConnection dbConnection_X; protected SqlCommand dbCommand_X; protected SqlDataAdapter dbAdapter_X; protected SqlTransaction_X
- 创建包装这些内在数据访问对象的方法。这些方法可以列出如下:
PrePair_X、Open_X、BegeinTransaction_X、ExcequteScalar_X、ExcequteNonQuery_X、
GetDataSet_X, ReuseCommand_X。为了建立数据库连接,你必须在 web.config 文件的 <appSettings> 部分下添加一个键,并在超类 DABasis 中读取并将其分配给一个受保护的变量。朋友们,就这些了,现在
我们的数据访问层已经准备好与数据库 X 一起工作了。
2.4 如何应用具有不同隔离级别的数据库事务?
DABasis 类通过以下方法支持数据库事务:
protected void BeginTransaction_L(IsolationLevel iLevel),
protected void Rollback_L(),protectedvoid Commit_L()(Figure 20).
void BeginTransaction_L(IsolationLevel iLevel) 方法是事务的起点,你可以用不同的隔离级别来启动事务。ANSI SQL 标准定义了四种事务隔离级别,SqlServer 全部支持它们。
让我们简要看一下所有四个隔离级别
- 未提交读(脏读):允许一个事务读取其他事务未提交和已提交的数据。
- 已提交读:允许一个事务只读取其他事务已提交的数据,SqlServer 默认使用此隔离级别。
- 可重复读:此隔离级别确保另一个事务不会更新你自己的事务数据,直到你提交或回滚你的事务。但你无法用此隔离级别防止幻读。(另一个事务可以向你自己的事务数据中添加一行)
- 可序列化:这是最高的隔离级别,它确保另一个事务不会更新、删除或插入你自己的事务数据。
///<summary>
/// Starts a transaction to the local server
/// with isolationlevel
///</summary>
protectedvoid BeginTransaction_L(IsolationLevel iLevel)
{
try
{
// assign the OleDbtransaction to the SqlCommand
this.dbTransaction_L = dbConnection_L.BeginTransaction(iLevel);
this.dbCommand_L.Transaction = this.dbTransaction_L;
}
catch(Exception oException)
{
string strMessage = "Occured in BeginTransaction_L()";
ErrorLog(strMessage,oException);
}
}
///<summary>
/// Rollbacks the transaction to the local server
///</summary>
protectedvoid Rollback_L()
{
try
{
this.dbTransaction_L.Rollback();
}
catch(Exception oException)
{
string strMessage = "Occured in RollBack_L()";
ErrorLog(strMessage,oException);
}
}
///<summary>
/// Commits the local server
///</summary>
protectedvoid Commit_L()
{
try
{
this.dbTransaction_L.Commit();
}
catch(Exception oException)
{
string strMessage = "Occured in Commit_L()";
ErrorLog(strMessage,oException);
}
}
图 20
让我们通过一个例子来解释这些方法是如何协同工作的。在这个例子中,我们将删除一个订单及其详情。由于主从关系的约束,我们必须先删除订单详情,然后再删除订单本身。使用单个存储过程“SPDelOrders”(参见图 21)并在一个 ExecutenonQuery 方法中调用它非常高效。
-- Used in DAPrototype
CREATE PROCEDURE SPDelOrders
(
@OrderID int
)
AS
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
DELETE [Order Details] WHERE OrderID = @OrderID
DELETE Orders WHERE OrderID = @OrderID
IF @@error > 0
ROLLBACK TRANSACTION
ELSE
COMMIT TRANSACTION
GO
图 21
但为了演示,我们将在数据访问层删除一个订单及其订单详情。我们可以通过修改 ExecuteNonQuery 模式来实现这个目标。我们将使用存储过程“SPDelODDemo”和“SPDelOrdersDemo”来分别删除订单详情和订单(图 22)。
-- store procedures delete order details and order
CREATE PROCEDURE SPDelODDemo
(
@OrderID int
)
AS
DELETE [Order Details] WHERE OrderID = @OrderID
/* Used in DAPrototype*/
CREATE PROCEDURE SPDelOrdersDemo
(
@OrderID int
)
AS
DELETE Orders WHERE OrderID = @OrderID
DAOrder 类中用于删除订单的方法
///<summary>
/// Deletes an order and its corresponding entries in order details .
/// Its is used to demonstrate transaction
///</summary>
///<param name=nOrderID></param>
public bool DeleteOrder(int nOrderID)
{
// At first, we will delete Order Details with the
// foreign key value nOrderID and after we
// will delete the corresponding Order. If this transaction fails,
// then we will roll back first transaction
bool bSuccess=false;
try
{
Monitor.Enter(this);
// Delete Order Detials with the OrderID
Prepair_L(“SPDelODDemo”);// Add the parametrs
SqlParameter pmOrderID = pmFactory_In.GetPMInt4(“@OrderID”);
pmOrderID.Value = nOrderID;
AddParameter_L(pmOrderID);
// open the connection to the local server
this.Open_L();
this.BeginTransaction_L(System.Data.IsolationLevel.Serializable);
int nOD = ExcecuteNonQuery_L();
// write on the WaringLog
StringBuilder strBuilder = new StringBuilder();
strBuilder.Append(nOD);
strBuilder.Append( Rows are deleted in [Order Details] with the OrderID );
strBuilder.Append(nOrderID);
this.WarningLog(strBuilder.ToString());
// delete Orders with the OrderID= nOrderID
ReuseCommand_L(“SPDelOrdersDemo”);// Add the parametrs
AddParameter_L(pmOrderID);
nOD = ExcecuteNonQuery_L();
// write on the WaringLog
strBuilder = new StringBuilder();
strBuilder.Append(an order was deleted with ID );
strBuilder.Append(nOrderID);
WarningLog(strBuilder.ToString());
// commit the transaction
this.Commit_L();
bSuccess=true;
}
catch(Exception oException)
{
// Rollback the transaction
bSuccess=false;
this.Rollback_L();
string strMessage = "An error occured in DAOrderDetail:DeleteOrder" ;
ErrorLog(strMessage,oException);
}
finally
{
Close_L();
Monitor.Exit(this);
}
return bSuccess;
}
图 22
让我们一步一步地分析方法 publicbool DAOrder.DeleteOrder(int nOrderID)
一步一步来。
第 1 步。
我们初始化数据库连接和一个 SqlCommand 对象,该对象使用存储过程来删除订单详情。然后,我们将查询参数添加到 SqlCommand 类的实例中。
Monitor.Enter(this);
// Delete Order Detials with the OrderID
Prepair_L(“SPDelODDemo”);// Add the parameters
SqlParameter pmOrderID = pmFactory_In.GetPMInt4(@OrderID);
pmOrderID.Value = nOrderID;
AddParameter_L(pmOrderID);
第 2 步。
打开数据库连接,使用 BeginTransaction_L(System.Data.IsolationLevel.Serializable) 方法开始事务。
并执行存储过程“SPDelODDemo”。
// open the connection to the local server
this.Open_L();
this.BeginTransaction_L(System.Data.IsolationLevel.Serializable);
int nOD = ExecuteNonQuery_L();
// write on the WaringLog
StringBuilder strBuilder = new StringBuilder();
strBuilder.Append(nOD);
strBuilder.Append( Rows are deleted in [Order Details] with the OrderID );
strBuilder.Append(nOrderID);
this.WarningLog(strBuilder.ToString
步骤 3
通过清除先前的查询参数来重新初始化 SqlCommand 对象,设置存储过程“SPDelOrdersDemo”并执行它。
// delete Orders with the OrderID= nOrderID
ReuseCommand_L(“SPDelOrdersDemo”);// Add the parametrs
AddParameter_L(pmOrderID);
nOD = ExecuteNonQuery_L();
// write on the WaringLogstrBuilder = new StringBuilder();
strBuilder.Append(an order was deleted with ID );
strBuilder.Append(nOrderID);
WarningLog(strBuilder.ToString());
步骤 4
如果 try 块中没有错误异常,那么事务将被提交,否则将在异常块中回滚。
// commit the transaction
this.Commit_L();
bSuccess=true;
}
catch(Exception oException)
{
// Rollback the transaction
bSuccess=false;
this.Rollback_L();
string strMessage = An error occured in DAOrderDetail:DeleteOrder ;
ErrorLog(strMessage,oException);
}
finally
{
Close_L();
Monitor.Exit(this);
}
2.5 如何插入和检索图像?
假设,我们有一个名为“Images”的表,具有以下属性:
PId char(5)
Picture image
我们现在可以使用存储过程“SPInImage”和“SPSelImage”从数据库中插入和检索图像。
CREATE PROCEDURE SPInImage
(
@PId char(5),
@Picture image
)
AS
IF NOT EXISTS(SELECT * From Images WHERE PId=@PId)
BEGIN
INSERT INTO Images
(PId,
Picture
)
VALUES
(
@PId,
@Picture
)
END
GO
CREATE PROCEDURE SPSelImage
(
@PId char(15)
)
AS
SELECT Picture FROM Images WHERE PId = @PId
GO
图 23
现在,我们可以使用方法 bool InsertImage(string strPId,byte[] imPicture) 和 byte[] GetImage(string strPId) 来调用这些存储过程(见图 24)
///<summary>
/// Inserts a picture to the table
///</summary>
///<param name="strPId"></param>
///<param name="imPicture"></param>
///<returns></returns>
publicbool InsertImage(string strPId,byte[] imPicture)
{
bool bInsert = false;
try
{
// lock the intrinsic DataAccess utilities
Monitor.Enter(this);
// Initialise DataAccess utilities for the local server
this.Prepair_L("SPInImage");
// Add the parameters to SqlCommand
SqlParameter pmPId = pmFactory_In.GetPMChar5("@PId");
pmPId.Value = strPId;
this.AddParameter_L(pmPId);
SqlParameter pmPicture = pmFactory_In.GetPMImage("@Picture");
pmPicture.Value = imPicture;
this.AddParameter_L(pmPicture);
// Excequte the query
this.Open_L();
int nExcequte = ExcecuteNonQuery_L();
if(nExcequte>0)
{
bInsert = true;
}
}
catch(Exception oException)
{
string strError = "An Error Occured in :InsertImage";
ErrorLog(strError,oException);
}
finally
{
this.Close_L();
Monitor.Exit(this);
}
return bInsert;
}
///<summary>
/// retrieves an image from the table “images”///</summary>///<param name="strPId"></param>
publicbyte[] GetImage(string strPId)
{
byte[] imPicture = null;
try
{
// lock the intrinsic DataAccess utilities
Monitor.Enter(this);
// Initialise DataAccess utilities for the local server
this.Prepair_L("SPSelImage");
// Add the parameters to SqlCommand
SqlParameter pmPId = pmFactory_In.GetPMChar15("@PId");
pmPId.Value = strPId;
this.AddParameter_L(pmPId);
// Excequte the query
this.Open_L();
object oScalar = this.ExcecuteScalar_L();
if(oScalar!=null)
{
imPicture = (byte[]) oScalar;
}
}
catch(Exception oException)
{
string strError = "An Error Occured in :GetImage";
ErrorLog(strError,oException);
}
finally
{
this.Close_L();
Monitor.Exit(this);
}
return imPicture;
}
图 24