摘要:
学习向 Microsoft .net 应用程序公开数据的最佳方式,以及如何实现一个有效的策略以便在分布式应用程序的层间传递数据。
简介
在设计分布式应用程序时需要确定如何访问和表示与该应用程序相关联的业务数据。本文提供一些指导原则以帮助您选择公开数据、保持数据和在应用程序的层间传递数据的最佳方式。
图 1 所示为分布式应用程序中的常见层。本文区分业务数据与使用这些数据的业务过程,并且仅在需要明确说明时讨论业务过程层。同样,本文仅在直接涉及数据表示方式(例如 Microsoft? ASP.NET Web 页面公开业务数据的方式)时讨论表示层。图 1 中使用了两个新术语:数据访问逻辑组件和业务实体组件。本文后面将解释这些术语。
图 1:分布式应用程序中数据的访问与表示
多数应用程序将数据存储在关系数据库中。除此之外还有其他数据存储方式,但本文重点讨论 .NET 应用程序与关系数据库交互的方式,而并不专门讨论它如何与平面文件、非关系数据库等其他数据存储中的数据进行交互。
本文明确区分保持逻辑与数据本身。将保持逻辑与数据区分开来的原因如下:
独立的数据保持组件可以将应用程序与数据源名称、连接信息、字段名等数据库相关内容隔离开。
现在的许多应用程序都采用 XML Web Services、Microsoft 消息队列(亦称 MSMQ)等松散耦合的、基于消息的技术。这些应用程序通常通过传递业务文档而不是传递对象进行通信。
为区分保持逻辑与数据本身,本文提出了两种不同的组件类型。
数据访问逻辑组件。数据访问逻辑组件从数据库中检索数据并把实体数据保存回数据库中。数据访问逻辑组件还包含实现数据相关操作所需的所有业务逻辑。
业务实体组件。数据用来表示产品、订单等现实世界中的业务实体。在应用程序中表示这种业务实体的方法非常多,例如 XML、DataSet、面向对象的自定义类等,这取决于应用程序的物理和逻辑设计限制。本文后面将详细讨论各种设计方案。
数据访问逻辑组件
数据访问逻辑组件代表调用程序提供对数据库执行以下任务的方法:
在数据库中创建记录
读取数据库中的记录并把业务实体数据返回给调用程序
使用调用程序提供的修改后的业务实体数据更新数据库中的记录
删除数据库中的记录
执行上述任务的方法通常称为“CRUD”方法,这是由各项任务的首字母组成的一个缩写词。
数据访问逻辑组件还提供对数据库实现业务逻辑的方法。例如,数据访问逻辑组件可能包含一个查找目录中本月销售额最高的产品的方法。
通常,数据访问逻辑组件访问一个单一数据库,并封装了针对该数据库中一个表或一组相关表的数据相关操作。例如,可以定义一个数据访问逻辑组件来处理数据库中的 Customer 表和 Address 表,同时定义另一个数据访问逻辑组件来处理 Orders 表和 OrderDetails 表。本文后面将讨论将数据访问逻辑组件映射到数据库表的设计决策。
表示业务实体
每个数据访问逻辑组件都处理一种特定类型的业务实体。例如,Customer 数据访问逻辑组件处理 Customer 业务实体。表示业务实体的方式很多,这取决于诸如以下因素:
是否需要把业务实体数据与 Microsoft Windows® 窗体或 ASP.NET 页面中的控件绑定在一起?
是否需要对业务实体数据执行排序或搜索操作?
应用程序是每次处理一个业务实体,还是通常处理一组业务实体?
是本地部署还是远程部署应用程序?
XML Web services 是否使用该业务实体?
性能、可缩放性、可维护性、编程方便性等非功能性要求的重要程度如何?
本文将概述以下实现选项的优缺点:
XML。使用 XML 字符串或 XML 文档对象模型 (DOM) 对象来表示业务实体数据。XML 是一种开放而灵活的数据表示格式,可用于集成各种类型的应用程序。
DataSet。DataSet 是缓存在内存中的表,它是从关系数据库或 XML 文档中获得的。数据访问逻辑组件可以使用 DataSet 来表示从数据库中检索到的业务实体数据,您可以在应用程序中使用该 DataSet。
有类型的 DataSet。有类型的 DataSet 是从 ADO.NET DataSet 类继承而来的类,它为访问表和 DataSet 中的列提供了具有严格类型的方法、事件和属性。
业务实体组件。这是一种自定义类,用于表示各种业务实体类型。您可以定义保存业务实体数据的字段,并定义将此数据向客户端应用程序公开的属性,然后使用在该类中定义的字段来定义方法以封装简单的业务逻辑。此选项并不通过 CRUD 方法实现与基础数据访问逻辑组件的数据传递,而是通过客户端应用程序直接与数据访问逻辑组件进行通信以执行 CRUD 操作。
带有 CRUD 行为的业务实体组件。按上述方法定义一个自定义实体类,并实现调用与此业务实体相关联的基础数据访问逻辑组件的 CRUD 方法。
注意:如果希望以一种更加面向对象的方式使用数据,可以使用另一种替代方法,即定义一个基于公共语言运行库的反射功能的对象保持层。您可以创建一个使用反射功能来读取对象属性的架构,并使用映射文件来描述对象与表之间的映射。然而,要有效地实现上述方法,需要大量的基础结构代码投入。对于 ISV 和解决方案提供商来说,这种投入或许可以接受,但对于大多数组织则不可行。有关这方面的讨论超出了本文的范围,这里不再论述。
技术因素
图 2 所示为影响数据访问逻辑组件和业务实体实现策略的一些技术因素。本文将分别讨论这些技术因素并提供相关建议。
图 2:影响数据访问逻辑组件和业务实体设计的技术因素
将关系数据映射到业务实体
数据库通常包含许多表,这些表之间的关系通过主键和外键来实现。当定义业务实体以在 .net 应用程序中表示这些数据时,必须确定如何把这些表映射到业务实体。
请考虑图 3 所示的假想零售商数据库。
图 3:假想的关系数据库中的表关系
下表总结了示例数据库中的关系类型。
关系类型
示例
说明
一对多
Customer:Address
Customer:Order
一个客户可以有多个地址,例如送货地址、帐单接收地址、联系地址等。
一个客户可以有多个订单。
多对多
Order:Product
一个订单可以包含许多产品,每种产品由 OrderDetails 表中的单独一行表示。同样,一种产品也可以出现在许多订单中。
当定义业务实体以在数据库中建立信息模型时,应考虑要如何在您的应用程序中使用这些信息。应当标识封装您的应用程序的功能的核心业务实体,而不是为每个表定义单独的业务实体。
该假想零售商的应用程序中的典型操作如下:
获取(或更新)客户的有关信息(包括地址)
获取客户的订单列表
获取特定订单的订购项目列表
创建新订单
获取(或更新)一个或一组产品的有关信息
为满足这些应用程序要求,该应用程序要处理三个逻辑业务实体:Customer、Order 和 Product。对于每个业务实体,都将定义一个单独的数据访问逻辑组件,如下所示:
Customer 数据访问逻辑组件。此类将为检索和修改 Customer 表和 Address 表中的数据提供服务。
Order 数据访问逻辑组件。此类将为检索和修改 Order 表和 OrderDetails 表中的数据提供服务。
Product 数据访问逻辑组件。此类将为检索和修改 Product 表中的数据提供服务。
图 4 所示为这些数据访问逻辑组件与它们所表示的数据库中的表之间的关系。
图 4:定义向 .NET 应用程序公开关系数据的数据访问逻辑组件
将关系数据映射到业务实体的建议
要将关系数据映射到业务实体,请考虑以下建议:
花些时间来分析您的应用程序的逻辑业务实体并为之建立模型,不要为每个表定义一个单独的业务实体。建立应用程序的工作方式模型的方法之一是使用统一建模语言 (UML)。UML 是一种形式设计注释,用于在面向对象的应用程序中建立对象模型,并获取有关对象如何表示自动过程、人机交互以及关联的信息。
不要定义单独的业务实体来表示数据库中的多对多表,可以通过在数据访问逻辑组件中实现的方法来公开这些关系。例如,前面示例中的 OrderDetails 表没有映射到单独的业务实体,而是通过在 Order 数据访问逻辑组件中封装 OrderDetails 表来实现 Order 与 Product 表之间的多对多关系。
如果具有返回特定业务实体类型的方法,请把这些方法放在该类型对应的数据访问逻辑组件中。例如,当检索一个客户的全部订单时,返回值为 Order 类型,因此应在 Order 数据访问逻辑组件中实现该功能。反之,当检索订购某特定产品的全部客户时,应在 Customer 数据访问逻辑组件中实现该功能。
数据访问逻辑组件通常访问来自单一数据源的数据。当需要聚合多个数据源的数据时,建议分别为访问每个数据源定义一个数据访问逻辑组件,这些组件可以由一个能够执行聚合任务的更高级业务过程组件来调用。建议采用这种方法的原因有二:
事务管理集中在业务过程组件中,不需要由数据访问逻辑组件显式控制。如果通过一个数据访问逻辑组件访问多个数据源,则需要把该数据访问逻辑组件作为事务处理的根,这会给仅读取数据的功能带来额外的系统开销。
通常,并不是应用程序的所有区域都需要聚合,并且通过分离对数据的访问,您可以单独使用该类型,也可以在必要时将其用作聚合的一部分。
实现数据访问逻辑组件
数据访问逻辑组件是一个无状态类,也就是说,所交换的所有消息都可以独立解释。调用之间不存在状态。数据访问逻辑组件为访问单一数据库(某些情况下可以是多个数据库,例如水平数据库分区)中的一个或多个相关表提供方法。通常,数据访问逻辑组件中的这些方法将调用存储过程以执行相应操作。
数据访问逻辑组件的主要目标之一是从调用应用程序中隐藏数据库的调用及格式特性。数据访问逻辑组件为这些应用程序提供封装的数据访问服务。具体地说,数据访问逻辑组件处理以下实现细节:
管理和封装锁定模式
正确处理安全性和授权问题
正确处理事务处理问题
执行数据分页
必要时执行数据相关路由
为非事务性数据的查询实现缓存策略(如果适用)
执行数据流处理和数据序列化
本节后面将详细讨论其中的某些问题。
数据访问逻辑组件的应用方案
图 5 所示为从各种应用程序类型(包括 Windows 窗体应用程序、ASP.NET 应用程序、XML Web Services 和业务过程)中调用数据访问逻辑组件的方式。根据应用程序的部署方式,这些调用可以是本地的,也可以是远程的。
图 5:数据访问逻辑组件的应用方案
实现数据访问逻辑组件类
数据访问逻辑组件使用 ADO.NET 执行 SQL 语句或调用存储过程。
如果您的应用程序包含多个数据访问逻辑组件,可以使用数据访问助手组件来简化数据访问逻辑组件类的实现。该组件可以帮助管理数据库连接、执行 SQL 命令以及缓存参数。数据访问逻辑组件仍然封装访问特定业务数据所需的逻辑,而数据访问助手组件则专注于数据访问 API 的开发和数据连接配置,从而帮助减少代码的重复。当使用 Microsoft SQL Server™ 数据库时,可在您的应用程序中将其用作一个通用的数据访问助手组件。图 6 所示为使用数据访问助手组件帮助实现数据访问逻辑组件的方法。
图 6: 使用数据访问助手组件实现数据访问逻辑组件
当存在所有数据访问逻辑组件公用的实用程序功能时,可以定义一个基本类以从中继承和扩展数据访问逻辑组件。
将数据访问逻辑组件类设计为可以为不同类型的客户端提供一致的接口。如果将数据访问逻辑组件设计为与当前及潜在的业务过程层的实现要求相兼容,可以减少必须实现的附加接口、接触面或映射层的数目。
要支持广泛的业务过程和应用程序,请考虑以下技术以便将数据传入和传出数据访问逻辑组件方法:
将业务实体数据传递给数据访问逻辑组件中的方法。您可以用多种不同的格式传递数据:作为一系列标量值、作为 XML 字符串、作为 DataSet 或作为自定义业务实体组件。
从数据访问逻辑组件中的方法返回业务实体数据。您可以用多种不同的格式返回数据:作为输出参数标量值、作为 XML 字符串、作为 DataSet、作为自定义业务实体组件或作为数据读取器。
以下各节将说明用于将业务实体数据传入和传出数据访问逻辑组件的各种方式以及每种方式的优缺点。这些信息有助于您根据自己特定的应用程序方案做出相应选择。
将标量值作为输入和输出传递
这种方法的优点如下:
抽象。调用程序只需要知道定义业务实体的数据,而不需要知道业务实体的具体类型或具体结构。
序列化。标量值本身支持序列化。
内存使用效率高。标量值只传递实际需要的数据。
性能。当处理实例数据时,标量值具有比本文所述的其他方法更高的性能。
这种方法的缺点如下:
紧密耦合与维护。架构的更改可能需要修改方法签名,这会影响调用代码。
实体集合。要向数据访问逻辑组件保存或更新多个实体,必须进行多次单独的方法调用。这在分布式环境中会给性能带来很大影响。
支持开放式并发。要支持开放式并发,必须在数据库中定义时间戳列并将其作为数据的一部分。
将 XML 字符串作为输入和输出传递
这种方法的优点如下:
松散耦合。调用程序只需要知道定义业务实体的数据和为业务实体提供元数据的架构。
集成。采用 XML 可以支持以各种方式(例如,.net 应用程序、BizTalk Orchestration 规则和第三方业务规则引擎)实现的调用程序。
业务实体集合。一个 XML 字符串可以包含多个业务实体的数据。
序列化。字符串本身支持序列化。
这种方法的缺点如下:
需要重新分析 XML 字符串。必须在接收端重新分析 XML 字符串。很大的 XML 字符串会影响性能。
内存使用效率低。XML 字符串比较繁琐,因而在需要传递大量数据时会降低内存使用效率。
支持开放式并发。要支持开放式并发,必须在数据库中定义时间戳列并将其作为 XML 数据的一部分。
将 DataSet 作为输入和输出传递
这种方法的优点如下:
固有功能。DataSet 提供了内置功能,可以处理开放式并发(以及数据适配器)并支持复杂的数据结构。此外,有类型的 DataSet 还提供了数据验证支持。
业务实体集合。DataSet 是为处理复杂的关系集合而设计的,因此不需要再编写自定义代码来实现这一功能。
维护。更改架构不会影响方法签名。然而,如果使用的有类型的 DataSet 和程序集具有严格名称,则必须按照新版本重新编译数据访问逻辑组件类,或在全局程序集缓存中使用发布者策略,或在配置文件中定义一个 <bindingRedirect> 元素。
序列化。DataSet 本身支持 XML 序列化,并且可以跨层序列化。
这种方法的缺点如下:
性能。实例化和封送处理 DataSet 会增加运行时负担。
表示单个业务实体。DataSet 是为处理一组数据而设计的。如果您的应用程序主要处理实例数据,则标量值或自定义实体是更好的方法,后者不会影响性能。
将自定义业务实体组件作为输入和输出传递
这种方法的优点如下:
维护。更改架构不会影响数据访问逻辑组件方法签名。然而,如果业务实体组件包含在严格命名的程序集中,就会出现与有类型的 DataSet 同样的问题。
业务实体集合。可以将自定义业务实体组件的数组和集合传入和传出方法。
这种方法的缺点如下:
支持开放式并发。要方便地支持开放式并发,必须在数据库中定义时间戳列并将其作为实例数据的一部分。
集成限制。当使用自定义业务实体组件作为数据访问逻辑组件的输入时,调用程序必须知道业务实体的类型,而这会限制不使用 .NET 的调用程序的集成。然而,如果调用程序使用自定义业务实体组件作为数据访问逻辑组件的输出,则上述问题并不会限制集成。例如,Web 方法可以返回从数据访问逻辑组件返回的自定义业务实体组件,并使用 XML 序列化自动将该业务实体组件序列化为 XML。
将数据读取器作为输出返回
这种方法的优点如下:
性能。当需要快速呈现数据时,这种方法具有性能优势,并且可以使用表示层代码部署数据访问逻辑组件。
这种方法的缺点如下:
远程。建议不要在远程方案中使用数据读取器,因为它可能会使客户端应用程序与数据库保持长时间的连接。
配合使用数据访问逻辑组件与存储过程
可以使用存储过程执行数据访问逻辑组件支持的许多数据访问任务。
优点
存储过程通常可以改善性能,因为数据库能够优化存储过程使用的数据访问计划并为以后的重新使用缓存该计划。
可以在数据库内分别设置各个存储过程的安全保护。管理员可以授予客户端执行某个存储过程的权限,而不授予任何基础表访问权限。
存储过程可以简化维护,因为修改存储过程通常比修改所部署的组件中的硬编码 SQL 语句要容易。然而,随着在存储过程中实现的业务逻辑的增多,上述优势会逐渐减弱。
存储过程增大了从基础数据库架构进行抽象的程度。存储过程的客户端与存储过程的实现细节和基础架构是彼此分离的。
存储过程会降低网络流量。应用程序可以按批执行 SQL 语句而不必发出多个 SQL 请求。
尽管存储过程具有上述优点,但仍有某些情况不适合使用存储过程。
缺点
如果逻辑全部在存储过程中实现,那么涉及广泛业务逻辑和处理的应用程序可能会给服务器带来过重负荷。这类处理包括数据传输、数据遍历、数据转换和大计算量操作。应把这类处理移到业务过程或数据访问逻辑组件中,与数据库服务器相比,它们具有更好的可缩放性。
不要把所有业务逻辑都放在存储过程中。如果必须在 T - SQL 中修改业务逻辑,应用程序的维护和灵活性将成为问题。例如,支持多个 RDBMS 的 ISV 应用程序不应当分别为每个系统维护存储过程。
通常,存储过程的编写与维护是一项专门技能,并非所有开发人员都能够掌握。这会造成项目开发计划的瓶颈。
配合使用数据访问逻辑组件与存储过程的建议
配合使用数据访问逻辑组件与存储过程时,请考虑以下建议:
公开存储过程。数据访问逻辑组件应当是向存储过程名称、参数、表、字段等数据库架构信息公开的仅有组件。业务实体实现应不需要知道或依赖于数据库架构。
使存储过程与数据访问逻辑组件相关联。每个存储过程只应被一个数据访问逻辑组件调用,并应与调用它的数据访问逻辑组件相关联。例如,假设一个客户向一个零售商订货。您可以编写一个名为 OrderInsert 的存储过程,用于在数据库中创建订单。在您的应用程序中,必须确定是从 Customer 数据访问逻辑组件还是从 Order 数据访问逻辑组件调用该存储过程。Order 数据访问逻辑组件处理所有与订单相关的任务,而 Customer 数据访问逻辑组件处理客户姓名、地址等客户信息,因此最好使用前者。
命名存储过程。为要使用的数据访问逻辑组件定义存储过程时,所选择的存储过程名称应当强调与之相关的数据访问逻辑组件。这种命名方法有助于识别哪个组件调用哪个存储过程,并为在 SQL 企业管理器中逻辑分组存储过程提供了一种方法。例如,可以事先编写名为 CustomerInsert、CustomerUpdate、CustomerGetByCustomerID、CustomerDelete 的存储过程供 Customer 数据访问逻辑组件使用,然后提供 CustomerGetAllInRegion 等更具体的存储过程以支持您的应用程序的业务功能。
注意:不要在存储过程名称前面使用前缀 sp_,这会降低性能。当调用一个以 sp_ 开头的存储过程时,SQL Server 始终会先检查 master 数据库,即使该存储过程已由数据库名称进行限定。
解决安全性问题。如果接受用户输入以动态执行查询,请不要通过没有使用参数的连接值来创建字符串。如果使用 sp_execute 执行结果字符串,或者不使用 sp_executesql 参数支持,则还应避免在存储过程中使用字符串连接。
管理锁定和并发
某些应用程序在更新数据库数据时采用“后进有效”(Last in Wins) 法。使用“后进有效”法更新数据库时不会将更新与原始记录相比较,因此可能会覆盖掉自上次刷新记录以来其他用户所做的所有更改。然而,有时应用程序却需要在执行更新之前确定数据自最初读取以来是否被更改。
数据访问逻辑组件可以实现管理锁定和并发的代码。管理锁定和并发的方法有两种:
保守式并发。为进行更新而读取某行数据的用户可以在数据源中对该行设置一个锁定。在该用户解除锁定之前,其他任何用户都不能更改该行。
开放式并发。用户在读取某行数据时不锁定该行。其他用户可以在同一时间自由访问该行。当用户要更新某行数据时,应用程序必须确定自该行被读取以来其他用户是否进行过更改。尝试更新已经过更改的记录会导致并发冲突。
使用保守式并发
保守式并发主要用于数据争用量大以及通过锁定来保护数据的成本低于发生并发冲突时回滚事务的成本的环境。如果锁定时间很短(例如在编程处理的记录中),则实现保守式并发效果最好。
保守式并发要求与数据库建立持久连接,并且因为记录可能被锁定较长时间,因此当用户与数据进行交互时,不能提供可缩放的性能。
使用开放式并发
开放式并发适用于数据争用量低或要求只读访问数据的环境。开放式并发可以减少所需锁定的数量,从而降低数据库服务器的负荷,提高数据库的性能。
开放式并发在 .net 中被广泛使用以满足移动和脱机应用程序的需要。在这种情况下,长时间锁定数据是不可行的。此外,保持记录锁定还要求与数据库服务器的持久连接,这在脱机应用程序中是不可能的。
测试开放式并发冲突
测试开放式并发冲突的方法有多种:
使用分布式时间戳。分布式时间戳适用于不要求协调的环境。在数据库的每个表中添加一个时间戳列或版本列。时间戳列与对表内容的查询一起返回。当试图更新时,数据库中的时间戳值将与被修改行中的原始时间戳值进行比较。如果这两个值匹配,则执行更新,同时时间戳列被更新为当前时间以反映更新。如果这两个值不匹配,则发生开放式并发冲突。
保留原始数据值的副本。在查询数据库的数据时保留原始数据值的一个副本。在更新数据库时,检查数据库的当前值是否与原始值匹配。
原始值保存在 DataSet 中,当更新数据库时,数据适配器可以使用该原始值执行开放式并发检查。
使用集中的时间戳。在数据库中定义一个集中的时间戳表,用于记录对任何表中的任何行的更新。例如,时间戳表可以显示以下信息:“2002 年 3 月 26 日下午 2:56 约翰更新了表 XYZ 中的行 1234”。
集中的时间戳适用于签出方案以及某些脱机客户端方案,其中可能需要明确的锁定所有者和替代管理。此外,集中的时间戳还可以根据需要提供审核。
手动实现开放式并发
请考虑以下 SQL 查询:
SELECT Column1, Column2, Column3 FROM Table1
要在更新 Table1 的行时测试开放式并发冲突,可以发出以下 UPDATE 语句:
UPDATE Table1 Set Column1 = @NewValueColumn1,
Set Column2 = @NewValueColumn2,
Set Column3 = @NewValueColumn3
WHERE Column1 = @OldValueColumn1 AND
Column2 = @OldValueColumn2 AND
Column3 = @OldValueColumn3
如果原始值与数据库中的值匹配,则执行更新。如果某个值被修改,WHERE 子句将无法找到相应匹配,从而更新将不会修改该行。您可以对此技术稍加变化,即只对特定列应用 WHERE 子句,使得如果自上次查询以来特定字段被更新,则不覆盖数据。
注意:请始终返回一个唯一标识查询中的一行的值,例如一个主关键字,以用于 UPDATE 语句的 WHERE 子句。这样可以确保 UPDATE 语句更新正确的行。
如果数据源中的列允许空值,则可能需要扩展 WHERE 子句,以便检查本地表与数据源中匹配的空引用。例如,以下 UPDATE 语句将验证本地行中的空引用(或值)是否仍然与数据源中的空引用(或值)相匹配。
UPDATE Table1 Set Column1 = @NewColumn1Value
WHERE (@OldColumn1Value IS NULL AND Column1 IS NULL) OR Column1 =
@OldColumn1Value
使用数据适配器和 DataSet 实现开放式并发
可以配合使用 DataAdapter.RowUpdated 事件与前面所述技术以通知您的应用程序发生了开放式并发冲突。每当试图更新 DataSet 中的修改过的行时,都将引发 RowUpdated 事件。可以使用 RowUpdated 事件添加特殊处理代码,包括发生异常时的处理、添加自定义错误信息以及添加重试逻辑。
RowUpdated 事件处理程序接收一个 RowUpdatedEventArgs 对象,该对象具有 RecordsAffected 属性,可以显示针对表中的一个修改过的行的更新命令会影响多少行。如果把更新命令设置为测试开放式并发,则当发生开放式并发冲突时,RecordsAffected 属性将为 0。设置 RowUpdatedEventArgs.Status 属性以表明要采取的操作;例如,可以把该属性设置为 UpdateStatus.SkipCurrentRow 以跳过对当前行的更新,但是继续更新该更新命令中的其他行。有关 RowUpdated 事件的详细信息,请参阅 Working with DataAdapter Events。
使用数据适配器测试并发错误的另一种方法是在调用 Update 方法之前把 DataAdapter.ContinueUpdateOnError 属性设置为 true。完成更新后,调用 DataTable 对象的 GetErrors 方法以确定哪些行发生了错误。然后,使用这些行的 RowError 属性找到特定的详细错误信息。有关如何处理行错误的详细信息,请参阅 Adding and Reading Row Error Information。
以下代码示例显示了 Customer 数据访问逻辑组件如何检查并发冲突。该示例假设客户端检索到了一个 DataSet 并修改了数据,然后把该 DataSet 传递给了数据访问逻辑组件中的 UpdateCustomer 方法。UpdateCustomer 方法将通过调用以下存储过程来更新相应的客户记录;仅当客户 ID 与公司名称未被修改时存储过程才能更新该客户记录:
CREATE PROCEDURE CustomerUpdate
{
@CompanyName varchar(30),
@oldCustomerID varchar(10),
@oldCompanyName varchar(30)
}
AS
UPDATE Customers Set CompanyName = @CompanyName
WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName
GO
在 UpdateCustomer 方法中,以下代码示例将一个数据适配器的 UpdateCommand 属性设置为测试开放式并发,然后使用 RowUpdated 事件测试开放式并发冲突。如果遇到开放式并发冲突,应用程序将通过设置要更新的行的 RowError 来表明开放式并发冲突。注意,传递给 UPDATE 命令中的 WHERE 子句的参数值被映射到 DataSet 中各相应列的原始值。
// CustomerDALC 类中的 UpdateCustomer 方法
public void UpdateCustomer(DataSet dsCustomer)
{
// 连接到 Northwind 数据库
SqlConnection cnNorthwind = new SqlConnection(
"Data source=localhost;Integrated security=SSPI;Initial
Catalog=northwind");
// 创建一个数据适配器以访问 Northwind 中的 Customers 表
SqlDataAdapter da = new SqlDataAdapter();
// 设置数据适配器的 UPDATE 命令,调用存储过程“UpdateCustomer”
da.UpdateCommand = new SqlCommand("CustomerUpdate", cnNorthwind);
da.UpdateCommand.CommandType = CommandType.StoredProcedure;
// 向数据适配器的 UPDATE 命令添加两个参数,
// 为 WHERE 子句指定信息(用于检查开放式并发冲突)
da.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30,
"CompanyName");
// 将 CustomerID 的原始值指定为第一个 WHERE 子句参数
SqlParameter myParm = da.UpdateCommand.Parameters.Add(
"@oldCustomerID", SqlDbType.NChar, 5,
"CustomerID");
myParm.SourceVersion = DataRowVersion.Original;
// 将 CustomerName 的原始值指定为第二个 WHERE 子句参数
myParm = da.UpdateCommand.Parameters.Add(
"@oldCompanyName", SqlDbType.NVarChar, 30,
"CompanyName");
myParm.SourceVersion = DataRowVersion.Original;
// 为 RowUpdated 事件添加一个处理程序
da.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);
// 更新数据库
da.Update(ds, "Customers");
foreach (DataRow myRow in ds.Tables["Customers"].Rows)
{
if (myRow.HasErrors)
Console.WriteLine(myRow[0] + " " + myRow.RowError);
}
}
// 处理 RowUpdated 事件的方法。 如果登记该事件但不处理它,
// 则引发一个 SQL 异常。
protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs
args)
{
if (args.RecordsAffected == 0)
{
args.Row.RowError = "遇到开放式并发冲突";
args.Status = UpdateStatus.SkipCurrentRow;
}
}
当在一个 SQL Server 存储过程中执行多个 SQL 语句时,出于性能原因,可以使用 SET NOCOUNT ON 选项。此选项将禁止 SQL Server 在每次执行完一条语句时都向客户端返回一条消息,从而可以降低网络流量。然而,这样将不能像前面的代码示例那样检查 RecordsAffected 属性。RecordsAffected 属性将始终为 1。另一种方法是在存储过程中返回 @@ROWCOUNT 函数(或将它指定为一个输出参数);@@ROWCOUNT 包含了存储过程中上一条语句完成时的记录数目,并且即使使用了 SET NOCOUNT ON,该函数也会被更新。因此,如果存储过程中执行的上一条 SQL 语句是实际的 UPDATE 语句,并且已经指定 @@ROWCOUNT 作为返回值,则可以对应用程序代码进行如下修改:
// 向数据适配器的 UPDATE 命令添加另一个参数来接收返回值。
// 可以任意命名该参数。
myParm = da.UpdateCommand.Parameters.Add("@RowCount", SqlDbType.Int);
myParm.Direction = ParameterDirection.ReturnValue;
// 将 OnRowUpdated 方法修改为检查该参数的值
// 而不是 RecordsAffected 属性。
protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs
args)
{
if (args.Command.Parameters["@RowCount"].Value == 0)
{
args.Row.RowError = "遇到开放式并发冲突";
args.Status = UpdateStatus.SkipCurrentRow;
}
}
COM 互操作性
如果希望数据访问逻辑组件类能够被 COM 客户端调用,则建议按前面所述的原则定义数据存取逻辑组件,并提供一个包装组件。然而,如果希望 COM 客户端能够访问数据访问逻辑组件,请考虑以下建议:
将该类及其成员定义为公共。
避免使用静态成员。
在托管代码中定义事件-源接口。
提供一个不使用参数的构造函数。
不要使用重载的方法,而使用多个名称不同的方法。
使用接口公开常用操作。
使用属性为类和成员提供附加 COM 信息。
在 .NET 代码引发的所有异常中包含 HRESULT 值。
在方法签名中使用自动兼容的数据类型。