适用于:
Microsoft® .NET 应用程序
摘要:
学习向 Microsoft .NET 应用程序公开数据的最佳方式,以及如何实现一个有效的策略以便在分布式应用程序的层间传递数据。(本文包含一些指向英文站点的链接。)
目录
简介
在设计分布式应用程序时需要确定如何访问和表示与该应用程序相关联的业务数据。本文提供一些指导原则以帮助您选择公开数据、保持数据和在应用程序的层间传递数据的最佳方式。
图1 所示为分布式应用程序中的常见层。本文区分业务数据与使用这些数据的业务过程,并且仅在需要明确说明时讨论业务过程层。同样,本文仅在直接涉及数据表示方式(例如 Microsoft® ASP.NET Web 页面公开业务数据的方式)时讨论表示层。图1 中使用了两个新术语:数据访问逻辑组件和业务实体组件。本文后面将解释这些术语。
图1:分布式应用程序中数据的访问与表示
多数应用程序将数据存储在关系数据库中。除此之外还有其他数据存储方式,但本文重点讨论 .NET 应用程序与关系数据库交互的方式,而并不专门讨论它如何与平面文件、非关系数据库等其他数据存储中的数据进行交互。
本文明确区分保持逻辑与数据本身。将保持逻辑与数据区分开来的原因如下:
- 独立的数据保持组件可以将应用程序与数据源名称、连接信息、字段名等数据库相关内容隔离开。
- 现在的许多应用程序都采用XML Web services、Microsoft消息队列(亦称 MSMQ)等松散耦合的、基于消息的技术。这些应用程序通常通过传递业务文档而不是传递对象进行通信。
注意:有关XML Web services的介绍,请参阅MSDN® Magazine 2002年3月号中的文章 .NET Web Services: Web Methods Make it Easy to Publish Your App's Interface over the Internet。有关消息队列的详细信息,请参阅“Message Queuing Overview”。
为区分保持逻辑与数据本身,本文提出了两种不同的组件类型。
- 数据访问逻辑组件。数据访问逻辑组件从数据库中检索数据并把实体数据保存回数据库中。数据访问逻辑组件还包含实现数据相关操作所需的所有业务逻辑。
- 业务实体组件。数据用来表示产品、订单等现实世界中的业务实体。在应用程序中表示这种业务实体的方法非常多,例如 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 的介绍,请参阅 .NET Data Access Architecture Guide 中的“Introducing ADO.NET”。
- 有类型的 DataSet。有类型的 DataSet 是从 ADO.NET DataSet 类继承而来的类,它为访问表和 DataSet 中的列提供了具有严格类型的方法、事件和属性。
- 业务实体组件。这是一种自定义类,用于表示各种业务实体类型。您可以定义保存业务实体数据的字段,并定义将此数据向客户端应用程序公开的属性,然后使用在该类中定义的字段来定义方法以封装简单的业务逻辑。此选项并不通过CRUD方法实现与基础数据访问逻辑组件的数据传递,而是通过客户端应用程序直接与数据访问逻辑组件进行通信以执行CRUD操作。
- 带有CRUD行为的业务实体组件。按上述方法定义一个自定义实体类,并实现调用与此业务实体相关联的基础数据访问逻辑组件的CRUD方法。
注意:如果希望以一种更加面向对象的方式使用数据,可以使用另一种替代方法,即定义一个基于公共语言运行库的反射功能的对象保持层。您可以创建一个使用反射功能来读取对象属性的架构,并使用映射文件来描述对象与表之间的映射。然而,要有效地实现上述方法,需要大量的基础结构代码投入。对于 ISV 和解决方案提供商来说,这种投入或许可以接受,但对于大多数组织则不可行。有关这方面的讨论超出了本文的范围,这里不再论述。
技术因素
图2 所示为影响数据访问逻辑组件和业务实体实现策略的一些技术因素。本文将分别讨论这些技术因素并提供相关建议。
图2:影响数据访问逻辑组件和业务实体设计的技术因素
将关系数据映射到业务实体
数据库通常包含许多表,这些表之间的关系通过主键和外键来实现。当定义业务实体以在 .NET 应用程序中表示这些数据时,必须确定如何把这些表映射到业务实体。
请考虑图 3 所示的假想零售商数据库。
图3:假想的关系数据库中的表关系
下表总结了示例数据库中的关系类型。
当定义业务实体以在数据库中建立信息模型时,应考虑要如何在您的应用程序中使用这些信息。应当标识封装您的应用程序的功能的核心业务实体,而不是为每个表定义单独的业务实体。
该假想零售商的应用程序中的典型操作如下:
- 获取(或更新)客户的有关信息(包括地址)
- 获取客户的订单列表
- 获取特定订单的订购项目列表
- 创建新订单
- 获取(或更新)一个或一组产品的有关信息
为满足这些应用程序要求,该应用程序要处理三个逻辑业务实体:Customer、Order 和 Product。对于每个业务实体,都将定义一个单独的数据访问逻辑组件,如下所示:
- Customer 数据访问逻辑组件。此类将为检索和修改 Customer 表和 Address 表中的数据提供服务。
- Order 数据访问逻辑组件。此类将为检索和修改 Order 表和 OrderDetails 表中的数据提供服务。
- Product 数据访问逻辑组件。此类将为检索和修改 Product 表中的数据提供服务。
图4 所示为这些数据访问逻辑组件与它们所表示的数据库中的表之间的关系。
图4:定义向 .NET 应用程序公开关系数据的数据访问逻辑组件
将关系数据映射到业务实体的建议
要将关系数据映射到业务实体,请考虑以下建议:
- 花些时间来分析您的应用程序的逻辑业务实体并为之建立模型,不要为每个表定义一个单独的业务实体。建立应用程序的工作方式模型的方法之一是使用统一建模语言 (UML)。UML 是一种形式设计注释,用于在面向对象的应用程序中建立对象模型,并获取有关对象如何表示自动过程、人机交互以及关联的信息。有关详细信息,请参阅 Modeling Your Application and Data。
- 不要定义单独的业务实体来表示数据库中的多对多表,可以通过在数据访问逻辑组件中实现的方法来公开这些关系。例如,前面示例中的 OrderDetails 表没有映射到单独的业务实体,而是通过在 Order 数据访问逻辑组件中封装 OrderDetails 表来实现 Order 与 Product 表之间的多对多关系。
- 如果具有返回特定业务实体类型的方法,请把这些方法放在该类型对应的数据访问逻辑组件中。例如,当检索一个客户的全部订单时,返回值为 Order 类型,因此应在 Order 数据访问逻辑组件中实现该功能。反之,当检索订购某特定产品的全部客户时,应在 Customer 数据访问逻辑组件中实现该功能。
- 数据访问逻辑组件通常访问来自单一数据源的数据。当需要聚合多个数据源的数据时,建议分别为访问每个数据源定义一个数据访问逻辑组件,这些组件可以由一个能够执行聚合任务的更高级业务过程组件来调用。建议采用这种方法的原因有二:
- 事务管理集中在业务过程组件中,不需要由数据访问逻辑组件显式控制。如果通过一个数据访问逻辑组件访问多个数据源,则需要把该数据访问逻辑组件作为事务处理的根,这会给仅读取数据的功能带来额外的系统开销。
- 通常,并不是应用程序的所有区域都需要聚合,并且通过分离对数据的访问,您可以单独使用该类型,也可以在必要时将其用作聚合的一部分。
实现数据访问逻辑组件
数据访问逻辑组件是一个无状态类,也就是说,所交换的所有消息都可以独立解释。调用之间不存在状态。数据访问逻辑组件为访问单一数据库(某些情况下可以是多个数据库,例如水平数据库分区)中的一个或多个相关表提供方法。通常,数据访问逻辑组件中的这些方法将调用存储过程以执行相应操作。
数据访问逻辑组件的主要目标之一是从调用应用程序中隐藏数据库的调用及格式特性。数据访问逻辑组件为这些应用程序提供封装的数据访问服务。具体地说,数据访问逻辑组件处理以下实现细节:
- 管理和封装锁定模式
- 正确处理安全性和授权问题
- 正确处理事务处理问题
- 执行数据分页
- 必要时执行数据相关路由
- 为非事务性数据的查询实现缓存策略(如果适用)
- 执行数据流处理和数据序列化
本节后面将详细讨论其中的某些问题。
数据访问逻辑组件的应用方案
图5 所示为从各种应用程序类型(包括 Windows 窗体应用程序、ASP.NET 应用程序、XML Web services 和业务过程)中调用数据访问逻辑组件的方式。根据应用程序的部署方式,这些调用可以是本地的,也可以是远程的。
图5:数据访问逻辑组件的应用方案(单击缩略图以查看大图像)
实现数据访问逻辑组件类
数据访问逻辑组件使用 ADO.NET 执行 SQL 语句或调用存储过程。有关数据访问逻辑组件类的示例,请参阅附录中的 如何定义数据访问逻辑组件类。
如果您的应用程序包含多个数据访问逻辑组件,可以使用数据访问助手组件来简化数据访问逻辑组件类的实现。该组件可以帮助管理数据库连接、执行 SQL 命令以及缓存参数。数据访问逻辑组件仍然封装访问特定业务数据所需的逻辑,而数据访问助手组件则专注于数据访问 API 的开发和数据连接配置,从而帮助减少代码的重复。Microsoft提供了 Data Access Application Block for .NET,当使用 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 以跳过对当前行的更新,但是继续更新该更新命令中的其他行。
使用数据适配器测试并发错误的另一种方法是在调用 Update 方法之前把 DataAdapter.ContinueUpdateOnError 属性设置为 true。完成更新后,调用 DataTable 对象的 GetErrors 方法以确定哪些行发生了错误。然后,使用这些行的 RowError 属性找到特定的详细错误信息。
以下代码示例显示了 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 值。
- 在方法签名中使用自动兼容的数据类型
实现业务实体
业务实体具有以下特点:
- 业务实体提供对业务数据及相关功能(在某些设计中)的状态编程访问。
- 业务实体可以使用具有复杂架构的数据来构建。这种数据通常来自数据库中的多个相关表。
- 业务实体数据可以作为业务过程的部分 I/O 参数传递。
- 业务实体可以是可序列化的,以保持它们的当前状态。例如,应用程序可能需要在本地磁盘、桌面数据库(如果应用程序脱机工作)或消息队列消息中存储实体数据。
- 业务实体不直接访问数据库。全部数据库访问都是由相关联的数据访问逻辑组件提供的。
- 业务实体不启动任何类型的事务处理。事务处理由使用业务实体的应用程序或业务过程来启动。
如本文前面所述,在您的应用程序中表示业务实体的方法有很多(从以数据为中心的模型到更加面向对象的表示法):
- XML
- 通用 DataSet
- 有类型的 DataSet
- 自定义业务实体组件
- 带有 CRUD 行为的自定义业务实体组件
以下各节将介绍如何使用这些格式来表示业务实体。为帮助您确定特定环境中最适宜的业务实体表示,以下各节将介绍如何为各业务实体格式执行以下任务:
- 组织业务实体集合
- 将业务实体数据绑定到用户界面控件
- 序列化业务实体数据
- 在层间传递业务实体数据
以下各节还针对非功能性要求(包括性能、效率、可缩放性和可扩展性)考虑了每种业务实体表示的适用性。
将业务实体表示为 XML
以下示例显示了如何将一个简单的业务实体表示为 XML。该业务实体包含一个产品。
<?xml version="1.0"?>
<Product xmlns="urn:aUniqueNamespace">
<ProductID>1</ProductID>
<ProductName>Chai</ProductName>
<QuantityPerUnit>10 boxes x 20 bags</QuantityPerUnit>
<UnitPrice>18.00</UnitPrice>
<UnitsInStock>39</UnitsInStock>
<UnitsOnOrder>0</UnitsOnOrder>
<ReorderLevel>10</ReorderLevel>
</Product>
|
有关详细信息,请参阅附录中的如何使用 XML表示数据的集合和层次结构。
当使用 XML 表示业务实体数据时,请考虑以下原则:
- 确定 XML 文档是包含单个业务实体还是包含一个业务实体集合。前面的示例表示的是单个 Product 业务实体。
- 使用一个命名空间唯一标识该 XML 文档,以避免与其他 XML 文档的内容发生命名冲突。前面的示例使用名为 urn:aUniqueNamespace 的默认命名空间。
- 为元素和属性选择合适的名称。前面的示例使用 Product 表的列名称,但并不要求一定这样。可以选择对您的应用程序有意义的名称。
- 使用以下方法之一以 XML 格式检索您的业务实体:
- 如果您使用的是 SQL Server 2000,则可以在查询或存储过程中使用 FOR XML 子句。在性能测试中,使用 FOR XML 只比返回 DataSet 稍微快一点。
- 检索 DataSet 并将其转换为 XML 流或以 XML 流的格式写出。这种方法会带来创建 DataSet 的系统开销和额外的转换开销(如果执行转换)。
- 使用输出参数或数据读取器构建一个 XML 文档。数据读取器是从数据库检索多个行的最快方法,但与构建 XML 相关联的过程可能会减弱这种性能优势。
将业务实体表示为 XML 的优点如下:
- 标准支持。XML 是 World Wide Web Consortium (W3C) 的标准数据表示格式。有关此标准的详细信息,请参阅 http://www.w3.org/xml。
- 灵活性。XML 能够表示信息的层次结构和集合。有关详细信息,请参阅附录中的如何使用 XML 表示数据的集合和层次结构。
- 互操作性。在所有平台上,XML 都是与外部各方及贸易伙伴交换信息的理想选择。如果 XML 数据将由 ASP.NET 应用程序或 Windows 窗体应用程序使用,则还可以把这些 XML 数据装载到一个 DataSet 中,以利用 DataSet 提供的数据绑定支持。
将业务实体表示为 XML 的缺点如下:
- 类型保真。XML 不支持类型保真。然而,对于简单的数据分类可以使用 XSD 架构。
- 验证 XML。要验证 XML,可以手动分析代码,或者使用 XSD 架构。但这两种方法都比较慢。有关如何使用 XSD 架构验证 XML 的示例,请参阅如何使用 XSD 架构验证 XML。
显示 XML。您不能将 XML 数据自动显示在用户界面上。可以编写一个 XSLT 样式表将数据转换为 DataSet;但样式表的编写比较麻烦。另一种方法是通过样式表将 XML 转换为 HTML 等可显示格式。有关详细信息,请参阅附录中的如何在 .NET 应用程序中编程应用样式表。
- 分析 XML。要分析 XML,可以使用文档对象模型 (DOM) 或 Microsoft .NET Framework 类库提供的 XmlReader 类。XmlReader 提供对 XML 数据的快速只读的、仅向前的访问,而 DOM 可以提供随机读/写访问,因此更灵活。然而,使用 DOM 分析 XML 文档的速度较慢;您必须创建一个 XmlDocument 实例(或另一个 XML 分析器类)并把整个 XML 文件装载到内存中。
- 排序 XML。您不能自动排序 XML 数据,而应使用以下技术之一:
- 按预先排好的顺序提供数据。这种方法不支持在调用应用程序中动态重新排序数据。
- 应用 XSLT 样式表动态排序数据。如果需要,可以使用 DOM 在运行时改变 XSLT 样式表中的排序条件。
- 将 XML 数据转换为 DataSet,并使用 DataView 对象排序和搜索数据元素。
- 使用专用字段。您不能选择隐藏信息。
将业务实体表示为通用 DataSet
通用 DataSet 是 DataSet 类的实例,它是在 ADO.NET 的 System.Data 命名空间中定义的。DataSet 对象包含一个或多个 DataTable 对象,用以表示数据访问逻辑组件从数据库检索到的信息。
图7 所示为用于 Product 业务实体的通用 DataSet 对象。该 DataSet 对象具有一个 DataTable,用于保存产品信息。该 DataTable 具有一个 UniqueConstraint 对象,用于将 ProductID 列标记为主键。DataTable 和 UniqueConstraint 对象是在数据访问逻辑组件中创建该 DataSet 时创建的。
图7:用于 Product 业务实体的通用 DataSet
图8 所示为用于 Order 业务实体的通用 DataSet 对象。此 DataSet 对象具有两个 DataTable 对象,分别保存订单信息和订单详细信息。每个 DataTable 具有一个对应的 UniqueConstraint 对象,用于标识表中的主键。此外,该 DataSet 还有一个 Relation 对象,用于将订单详细信息与订单相关联。
图8:用于 Order 业务实体的通用 DataSet
以下代码显示了如何从数据访问逻辑组件检索通用 DataSet ,然后将该 DataSet 绑定到 DataGrid 控件,再将该 DataSet 传递到数据访问逻辑组件以保存对数据所做的更改:
// 创建 ProductDALC 对象
ProductDALC dalcProduct = new ProductDALC();
// 对 ProductDALC 调用一个方法以获取一个包含全部产品信息的 DataSet
DataSet dsProducts = dalcProduct.GetProducts();
// 在客户端中使用 DataSet。 例如,把该 DataSet 绑定到用户界面控件
dataGrid1.DataSource = dsProducts.Tables[0].DefaultView;
dataGrid1.DataBind();
// 然后,把更新后的 DataSet 传递给 ProductDALC,将更改
// 保存到数据库
dalcProduct.UpdateProducts(dsProducts);
|
您还可以在运行时查询和修改 DataSet 中的表、约束及关系。
将业务实体表示为通用 DataSet 的优点如下:
- 灵活性。DataSet 可以包含数据的集合,能够表示复杂的数据关系。
- 序列化。在层间传递时,DataSet 本身支持序列化。
- 数据绑定。可以把 DataSet 绑定到 ASP.NET 应用程序和 Windows 窗体应用程序的任意用户界面控件。
- 排序与过滤。可以使用 DataView 对象排序和过滤 DataSet。应用程序可以为同一个 DataSet 创建多个 DataView 对象,以便用不同方式查看数据。
- 与 XML 的互换性。可以用 XML 格式读写 DataSet。这种方法在远程和脱机应用程序中很有用,它可以用 XML 格式接收 DataSet,然后在本地重新创建该 DataSet 对象。应用程序在与数据库断开连接后,还可以将 DataSet 保持为 XML 格式。
- 元数据的可用性。可以用 XSD 架构的形式为 DataSet 提供完整的元数据。还可以使用 DataSet、DataTable、DataColumn、Constraint 和 Relation 类中的方法以编程方式为 DataSet 获取元数据。
- 开放式并发。在更新数据时,可以配合使用数据适配器与 DataSet 以方便地执行开放式并发检查。
- 可扩展性。如果修改了数据库架构,则适当情况下数据访问逻辑组件中的方法可以创建包含修改后的 DataTable 和 DataRelation 对象的 DataSet。数据访问逻辑组件方法签名并不改变。可以将调用应用程序修改为使用该 DataSet 中的这些新元素。
将业务实体表示为通用 DataSet 的缺点如下:
- 客户端代码必须通过 DataSet 中的集合访问数据。要访问 DataSet 中的表,客户端代码必须使用整数索引生成器或字符串索引生成器来索引 DataTable 集合。要访问特定列,必须使用列编号或列名称索引 DataColumn 集合。以下示例显示了如何访问 Products 表中第一行的 ProductName 列:
// 获取所调用的名为 dsProducts 的 DataSet 的第一行的
// 产品名称。 注意,该集合是基于零的。
String str = (String)dsProducts.Tables["Products"].Rows[0]["ProductName"];
...
|
注意:这里没有这些索引生成器的编译时检查。如果指定一个无效的表名称、列名称或列类型,会在运行时捕获该错误。使用通用 DataSet 时不支持 IntelliSense。
- 实例化和封送处理的成本很高。DataSet 需要创建多个子对象(DataTable、DataRow 和 DataColumn),这意味着在实例化和封送处理时,DataSet 会比 XML 字符串或自定义实体组件花费更长的时间。随着数据量的增大,创建 DataSet 内部结构的系统开销将明显少于将数据填充到 DataSet 中所需的开销,因此 DataSet 的相对性能会随之提高。
- 专用字段。您不能选择隐藏信息。
将业务实体表示为有类型的 DataSet
有类型的 DataSet 是包含具有严格类型的方法、属性和类型定义以公开 DataSet 中的数据和元数据的类。有关如何创建有类型的 DataSet 的示例,请参阅附录中的如何创建有类型的 DataSet。
下面列出了有类型的 DataSet 与通用 DataSet 相比的优缺点。注意,有类型的 DataSet 的实例化和封送处理性能与通用 DataSet 基本相同。
将业务实体表示为有类型的 DataSet 的优点如下:
- 代码易读。要访问有类型的 DataSet 中的表和列,可以使用有类型的方法和属性,如以下代码所示:
...
// 获取所调用的名为 dsProducts 的有类型的 DataSet 的第一行的
// 产品名称。 注意,该集合是基于零的。
String str = dsProducts.Products[0].ProductName;
...
|
在本示例中,dsProducts 是有类型的 DataSet 的一个实例。该 DataSet 有一个 DataTable,它由一个命名为 Products 的属性公开。该 DataTable 中的列由 ProductName 等属性公开,后者返回列的相应数据类型(而不仅仅返回对象)。
有类型的方法和属性的提供使得使用有类型的 DataSet 比使用通用 DataSet 更方便。使用有类型的 DataSet 时,IntelliSense 将可用。
- 编译时类型检查。无效的表名称和列名称将在编译时而不是在运行时检测。
将业务实体表示为有类型的 DataSet 的缺点如下:
- 部署。必须将包含有类型的 DataSet 类的程序集部署到使用业务实体的所有层。
- 支持企业服务 (COM+) 调用程序。如果有类型的 DataSet 将由 COM+ 客户端使用,则必须为包含该有类型的 DataSet 类的程序集提供一个严格名称,并且必须在客户端计算机上注册。通常,该程序集安装在全局程序集缓存中。这些也是自定义实体类所要求的步骤,如本文后面所述。
- 可扩展性问题。如果修改了数据库架构,则可能需要重新生成有类型的 DataSet 类以支持新架构。重新生成过程将不会保留在有类型的 DataSet 类中实现的任何自定义代码。必须将包含有类型的 DataSet 类的程序集重新部署到所有客户端应用程序中。
- 实例化。您不能使用 new 运算符来实例化类型。
- 继承。有类型的 DataSet 必须从 DataSet 类继承,这会禁止使用任何其他基本类。
定义自定义业务实体组件
表示业务实体的自定义类通常包含以下成员:
- 用于在本地缓存业务实体的数据的专用字段。这些字段在数据访问逻辑组件从数据库检索数据时保存数据库数据的一个快照。
- 用于访问实体的状态和访问实体内数据的子集及层次结构的公共属性。这些属性的名称可以与数据库的列名称相同,但这并不是一个绝对要求。可以根据您的应用程序的需要选择属性名,而不必使用数据库中的名称。
- 用以使用实体组件中的数据执行本地化处理的方法和属性。
- 用以通知实体组件内部状态变化的事件。
图9 所示为使用自定义实体类的方法。注意,实体类并不知道数据访问逻辑组件或基础数据库;所有数据库访问都由数据访问逻辑组件执行,以集中数据访问策略和业务逻辑。此外,在层间传递业务实体数据的方式与表示业务实体的格式也没有直接关系;例如,可以在本地将业务实体表示为对象,而用另一种方法(如标量值或 XML)将业务实体数据传递到其他层。
图9:自定义业务实体组件的作用
定义自定义业务实体组件的建议
在实现自定义实体组件时,请考虑以下建议:
- 选择使用结构还是使用类。对于不包含分层数据或集合的简单业务实体,可以考虑定义一个结构来表示业务实体。对于复杂的业务实体或要求继承的业务实体,可将实体定义为类。
- 表示业务实体的状态。对于数字、字符串等简单值,可以使用等价的 .NET 数据类型来定义字段。有关说明如何定义自定义实体的代码示例,请参阅附录中的如何定义业务实体组件。
- 表示自定义业务实体组件中的子集合和层次结构。表示自定义实体中的数据的子集合和层次结构的方法有两种:
- .NET 集合(例如 ArrayList)。.NET 集合类为大小可调的集合提供了一个方便的编程模型,还为将数据绑定到用户界面控件提供了内置的支持。
- DataSet。DataSet 适合于存储来自关系数据库或 XML 文件的数据的集合和层次结构。此外,如果需要过滤、排序或绑定子集合,也应首选 DataSet。
- 有关说明如何表示自定义实体中数据的集合和层次结构的代码示例,请参阅附录中的如何表示自定义实体中数据的集合和层次结构。
- 支持用户界面客户端的数据绑定。如果自定义实体将要由用户界面使用并且希望利用自动数据绑定,可能需要在自定义实体中实现数据绑定。请考虑以下方案:
- Windows 窗体中的数据绑定。您可以将实体实例的数据绑定到控件而不必在自定义实体中实现数据绑定接口。也可以绑定实体的数组或 .NET 集合。
- Web 窗体中的数据绑定。如果不实现 IBindingList 接口,则不能将实体实例的数据绑定到 Web 窗体中的控件。然而,如果只想绑定集合,则可以使用数组或 .NET 集合而不必在自定义实体中实现 IBindingList 接口。
有关说明如何将自定义实体绑定到用户界面控件的代码示例,请参阅附录中的如何将业务实体组件绑定到用户界面控件。
- 公开内部数据更改的事件。公开事件可以获得丰富的客户端用户界面设计,因为它使得无论数据显示在哪里都可以对其进行刷新。事件应当只针对内部状态,而不是针对服务器上的数据更改。有关说明如何公开自定义实体类中的事件的代码示例,请参阅附录中的如何公开业务实体组件中的事件。
- 使业务实体可序列化。使业务实体可序列化可以将业务实体的状态保持在中间状态而不进行数据库交互。这样可以方便脱机应用程序的开发和复杂用户界面过程的设计,即在完成前不会影响业务数据。序列化有两种类型:
- 使用 XmlSerializer 类进行 XML 序列化。如果只需要把公共字段和公共读/写属性序列化为 XML,则可以使用 XML 序列化。注意,如果从 Web 服务返回业务实体数据,对象将通过 XML 序列化自动序列化为 XML。
您可以对业务实体执行 XML 序列化而无需在实体中实现任何附加代码。然而,只有对象中的公共字段和公共读/写属性被序列化为 XML。专用字段、索引生成器、专用属性、只读属性及对象图不会被序列化。您可以使用自定义实体中的属性控制结果 XML。有关将自定义实体组件序列化为 XML 格式的详细信息,请参阅附录中的如何将业务实体组件序列化为 XML 格式。
- 使用 BinaryFormatter 或 SoapFormatter 类进行格式序列化。如果需要序列化对象的所有公共字段、专用字段及对象图,或者需要与远程服务器之间传递实体组件,则可以使用格式序列化。
格式类将序列化对象的所有公共和专用字段及属性。BinaryFormatter 将对象序列化为二进制格式,SoapFormatter 将对象序列化为 SOAP 格式。使用 BinaryFormatter 的序列化比使用 SoapFormatter 的序列化速度要快。要使用任何一个格式类,都必须将实体类标记为 [Serializable] 属性。如果需要显式控制序列化格式,您的类还必须实现 ISerializable 接口。有关如何使用格式序列化的详细信息,请参阅附录中的如何将业务实体组件序列化为二进制格式及如何将业务实体组件序列化为 SOAP 格式。
注意:还原序列化某个对象时,不会调用默认的构造函数。对还原序列化添加这项约束,是出于性能方面的考虑。
定义自定义实体的优点如下:
- 代码易读。要访问自定义实体类中的数据,可以使用有类型的方法和属性,如以下代码所示:
// 创建一个 ProductDALC 对象
ProductDALC dalcProduct = new ProductDALC();
// 使用该 ProductDALC 对象创建和填充一个 ProductEntity 对象。
// 此代码假设 ProductDALC 类有一个名为 GetProduct 的方法,
// 该方法使用 Product ID 作参数(本例中为 21),并返回一个
// 包含该产品的所有数据的 ProductEntity 对象。
ProductEntity aProduct = dalcProduct.GetProduct(21);
// 更改该产品的产品名称
aProduct.ProductName = "Roasted Coffee Beans";
|
在上述示例中,产品是一个名为 ProductEntity 的自定义实体类的一个实例。ProductDALC 类有一个名为 GetProduct 的方法,后者创建一个 ProductEntity 对象,将某个特定产品的数据填充到该对象,然后返回 ProductEntity 对象。调用应用程序可以使用 ProductName 等属性访问 ProductEntity 对象中的数据,并且可以调用方法以操作该对象。
- 封装。自定义实体可以包含方法以封装简单的业务规则。这些方法操作缓存在实体组件中的业务实体数据,而不是访问数据库中的实时数据。请考虑以下示例:
// 调用一个在 ProductEntity 类中定义的方法。
aProduct.IncreaseUnitPriceBy(1.50);
|
在上述示例中,调用应用程序对 ProductEntity 对象调用一个名为 IncreaseUnitPriceBy 的方法。在调用应用程序对 ProductDALC 对象调用相应方法,从而将 ProductEntity 对象保存到数据库之前,这一更改并不是永久性的。
- 构建复杂系统的模型。在构建复杂域问题(在不同业务实体之间存在很多交互)的模型时,可以定义自定义实体类,从而将复杂性隐藏在经过很好定义的类接口的后面。
- 本地化验证。自定义实体类可以在其属性存取器中执行简单的验证测试以检测无效的业务实体数据。有关详细信息,请参阅如何在业务实体组件的属性存取器中验证数据。
- 专用字段。您可以隐藏不希望向调用程序公开的信息。
定义自定义实体的缺点如下:
- 业务实体集合。自定义实体表示的是单个业务实体,而不是一个业务实体集合。要保存多个业务实体,调用应用程序必须创建一个数组或一个 .NET 集合。
- 序列化。您必须在自定义实体中实现自己的序列化机制。可以使用属性来控制实体组件的序列化方式,也可以通过实现 ISerializable 接口来控制自己的序列化。
- 表示业务实体中的复杂关系和层次结构。您必须在业务实体组件中实现自己的关系和层次结构表示机制。如前面所述,DataSet 通常是实现这一目的的最简单方式。
- 搜索和排序数据。您必须定义自己的机制来支持实体的搜索和排序。例如,可以通过实现 IComparable 接口以便将实体组件保存在一个 SortedList 集合或 Hashtable 集合中。
- 部署。您必须在所有物理层部署包含自定义实体的程序集。
- 支持企业服务 (COM+) 客户端。如果一个自定义实体将由 COM+ 客户端使用,则必须为包含该实体的程序集提供一个严格名称,并且必须在客户端计算机上注册。通常,该程序集安装在全局程序集缓存中。
- 可扩展性问题。如果修改了数据库架构,则可能需要修改自定义实体类并重新部署程序集。
定义带有 CRUD 行为的自定义业务实体组件
在定义一个自定义实体时,可以提供方法以完全封装对基础数据访问逻辑组件的 CRUD 操作。这是比较传统的面向对象的方法,可能适用于复杂的对象域。客户端应用程序不再直接访问数据访问逻辑组件类,而是创建一个实体组件并对该实体组件调用 CRUD 方法。这些方法将调用基础的数据访问逻辑组件。
图10 所示为带有 CRUD 行为的自定义实体类的作用。
图10:带有 CRUD 行为的自定义业务实体组件的作用
定义带有 CRUD 行为的自定义实体类的优点如下:
- 封装。自定义实体可以封装由基础数据访问逻辑组件定义的操作。
- 与调用程序的接口。调用程序必须只处理一个接口来保持业务实体数据。不必直接访问数据访问逻辑组件。
- 专用字段。您可以隐藏不希望向调用程序公开的信息。
定义带有 CRUD 行为的自定义实体类的缺点如下:
- 处理业务实体集合。自定义实体中的方法属于单个业务实体实例。要支持业务实体集合,可以定义静态方法以读取或返回一个数组或一个实体组件集合。
- 开发时间长。传统的面向对象方法通常比使用现有对象(如 DataSet)需要更多的设计和开发工作。
表示数据和在层间传递数据的建议
在您的应用程序中表示数据的方式以及在层间传递数据的方式不一定要相同。然而,一套一致而有限的格式能够降低对附加转换层的需要,从而提高性能并方便维护。
应根据自己特定的应用程序要求和操作数据的方式选择数据格式。这里并没有一个通用的表示方式,特别是由于当今的许多应用程序都需要支持多个调用程序。然而,我们还是建议遵循以下一般原则:
- 如果您的应用程序主要处理集合并需要排序、搜索和数据绑定等功能,则建议采用 DataSet。但如果应用程序处理实例数据,则采用标量值的效果会更好。
- 如果您的应用程序主要处理实例数据,则自定义业务实体组件可能是最佳选择,因为它们可以消除一个 DataSet 表示一行时的系统开销。
- 大多数情况下,应把应用程序设计为使用 XML 文档、DataSet 等以数据为中心的格式。可以利用 DataSet 提供的灵活性及固有功能来更方便地支持多个客户端、减少自定义代码的数量并使用为大多数开发人员所熟知的编程 API。虽然以面向对象的方式操作数据有很多好处,但自定义编码复杂的业务实体会使开发和维护成本随所提供功能的数量成比例增加。
事务处理
当今的大多数应用程序都需要支持事务处理以保持系统数据的完整性。事务处理的管理方法有多种,但每种方法都可归于以下两种基本编程模型之一:
- 手动事务处理。直接在组件代码或存储过程中编写使用 ADO.NET 或 Transact-SQL 事务处理支持功能的代码。
- 自动事务处理。使用企业服务 (COM+) 为 .NET 类添加声明属性以便在运行时指定对象的事务性要求。使用这种模型可以方便地配置多个组件以执行同一事务中的工作。
本节提供一些指导原则和建议,帮助您在数据访问逻辑组件和业务实体组件中实现事务处理支持。
实现事务处理
在大多数环境中,事务处理的根本是业务过程而不是数据访问逻辑组件或业务实体组件。这是因为业务过程一般要求事务处理跨多个业务实体而不仅仅是单个业务实体。
然而,也可能出现在没有高层次业务过程的帮助下对单个业务实体执行事务性操作的情况。例如,要把一个新客户添加到前面讨论的数据库中,您必须执行以下操作:
- 在 Customer 表中插入新的一行。
- 在 Address 表中插入新的一行或多行。
只有这两个操作都成功后客户才会被添加到数据库中。如果 Customer 业务实体不会成为启动该事务处理的更大的业务过程的一部分,则应在 Customer 业务实体中使用手动事务处理。手动事务处理不要求与 Microsoft 分布式事务处理协调器 (DTC) 之间进行任何进程间通信,因此比自动事务处理要快得多。
图11 所示为确定使用手动事务处理还是自动事务处理的方法。由于 COM+ 事务处理的系统开销,建议将事务处理放到数据库中并在存储过程中控制事务性行为(如果可能)。
图11:确定如何实现事务处理
注意:如果从基于 ASP.NET 的客户端进行调用,并且没有用于启动事务处理的业务过程,则您可能会从 ASP.NET 代码中启动该事务处理。这种设计并不好;您决不能从基于 ASP.NET 的客户端启动事务处理,而应将数据的展示与业务过程相分离。此外,由于网络滞后等问题还会导致性能问题,因为这是要实际部署在其他层上的最常见的层。
在数据访问逻辑组件中使用手动事务处理的建议
在数据访问逻辑组件中实现手动事务处理时,请考虑以下建议:
- 尽可能在存储过程中执行处理。使用 Transact-SQL 语句 BEGIN TRANSACTION、END TRANSACTION 和 ROLLBACK TRANSACTION 控制事务处理。
- 如果没有使用存储过程,并且也不会从业务过程中调用数据访问逻辑组件,则可以使用 ADO.NET 来编程控制事务处理。
在数据访问逻辑组件中使用自动事务处理的建议
虽然 COM+ 事务处理会带来一些系统开销,但自动事务处理能够提供比手动事务处理更简单的编程模式,而且在事务处理跨多个分布式数据源(与 DTC 一起工作)时必须使用自动事务处理。在数据访问逻辑组件中实现自动事务处理时,请考虑以下建议:
- 数据访问逻辑组件必须是从 System.EnterpriseServices 命名空间中的 ServicedComponent 类继承而来。注意,使用 COM+ 服务注册的所有程序集都必须具有严格的名称。
- 使用 Transaction(TransactionOption.Supported) 属性注释数据访问逻辑组件,以便可以在同一组件中执行读写操作。与 Transaction(TransactionOption.Required) 不同,此选项在不需要事务处理时避免了不必要的系统开销,而前者始终会要求事务处理。
以下代码示例显示了如何在数据访问逻辑组件类中支持自动事务处理:
using System.EnterpriseServices;
[Transaction(TransactionOption.Supported)]
public class CustomerDALC : ServicedComponent
{
...
}
|
如果使用自动事务处理,则数据访问逻辑组件应在事务处理中表明操作是否成功。如果要隐式表明,应使用 AutoComplete 属性注释您的方法并在操作失败时发出一个异常。如果要显式表明,应对 ContextUtil 类调用 SetComplete 或 SetAbort 方法。
在业务实体组件中使用自动事务处理
在实现带有行为的自定义业务实体组件时,可以使用自动事务处理来指定这些对象的事务性行为。有关使用自动事务处理指定业务实体组件事务性行为的建议与前述有关在数据访问逻辑组件中实现自动事务处理的建议相同。
注意:如果业务实体组件不包含任何要求其在事务处理中表明是否成功的业务逻辑,则它可以忽略事务处理环境。自定义业务实体组件不需要从 ServicedComponent 继承;事务处理环境仍将继续其流程,但实体组件将忽略事务处理环境。
验证
您可以在应用程序的许多层上进行数据验证。各层适用不同的验证类型:
- 在提交数据之前,客户端应用程序可以在本地验证业务实体数据。
- 使用 XSD 架构接收业务文档时,业务过程可以验证这些文档。
- 数据访问逻辑组件和存储过程可以验证数据,以确保引用的完整性并强制遵循约束以及重要的业务规则。
常用验证有两种:
- 即时点验证。这是在一个特定时点执行的验证。例如,在接收 XML 文档时由业务过程对其进行验证。
- 连续验证。这是在应用程序的许多不同层次上持续进行的一种验证。连续验证的示例包括:
- 用户界面可以指定字段的最大长度以防止用户输入过长的字符串。
- DataSet 可以指定数据列的最大长度。
- 自定义业务实体组件可以对实体数据执行范围检查、长度检查、非空检查以及其他简单测试。
- 数据访问逻辑组件、存储过程和数据库本身可以执行类似的测试,以便在将数据保存到数据库之前确保其有效性。
有时,您可能希望实现额外的聚合过程或转换过程。这种方法在验证和转换经常变化时可能很有用,但会损失性能。例如,如果一个 ISV 想要使用相同的组件支持数据库架构的两个版本,则您可以创建一个单独的组件来执行两个数据库架构版本之间的验证和转换。
如何使用 XSD 架构验证 XML
要使用 XSD 架构验证 XML 文档,请执行以下步骤:
- 创建一个 XmlValidatingReader 对象作为 XmlTextReader 对象的包装,如以下代码所示:
' 创建 XmlValidatingReader 对象,以读取和验证 Product.xml
XmlTextReader tr = new XmlTextReader("Product.xml");
XmlValidatingReader vr = new XmlValidatingReader(tr);
|
- 通过使用 ValidationType 枚举指定所需的验证类型。.NET Framework 支持三种类型的 XML 验证:
文档类型定义 (DTD);指定 ValidationType.DTD
Microsoft XML 精简数据 (XDR) 架构;指定 ValidationType.XDR
W3C 标准 XSD 架构;指定 ValidationType.Schema
以下代码显示了 ValidationType 枚举的使用:
vr.ValidationType = ValidationType.Schema; ' 指定 XSD 架构验证
|
- 注册一个验证事件处理程序方法,如以下代码所示:
vr.ValidationEventHandler += new ValidationEventHandler(MyHandlerMethod);
|
- 提供一个验证事件处理程序方法的实现,如以下代码所示:
public void MyHandlerMethod(object sender, ValidationEventArgs e)
{
Console.WriteLine("验证错误:" + e.Message);
}
|
- 读取和验证文档,如以下代码所示。验证错误将被验证事件处理程序方法拾取。
try
{
while (vr.Read())
{
// 适当处理 XML 数据...
}
}
catch (XmlException ex)
{
Console.WriteLine("XmlException: " + ex.Message);
}
vr.Close();
|
如何在业务实体组件的属性存取器中验证数据
以下代码片段显示了如何在自定义实体的属性存取器中进行简单验证。如果验证测试失败,您可以发出一个异常以显示问题的性质。也可以在属性存取器集合中使用正则表达式来验证特定的数据和格式。
public class ProductDALC
{
...
public short ReorderLevel
{
get { return reorderLevel; }
}
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("ReorderLevel 不能为负数。");
}
reorderLevel = value;
}
// 加上 ProductDALC 类中的其他成员...
}
|
异常管理
当 .NET 应用程序出现错误时,通常的建议是发出异常而不是从方法返回错误值。这一建议暗示了您编写数据访问逻辑组件和业务实体组件的方式。异常大体上有两种:
技术异常,它包括:
- ADO.NET
- 数据库连接
- 资源(如数据库、网络共享、消息队列等)不可用
业务逻辑异常,它包括:
- 验证错误
- 实现业务逻辑的存储过程中的错误
在数据访问逻辑组件中管理异常的建议
数据访问逻辑组件应该传播异常,并且仅在能够使客户端对异常的管理更加容易时才包装异常类型。将异常包装为两种主要异常类型(技术异常和业务异常)有利于各种可能的调用程序的异常处理结构和异常发布逻辑。
您的应用程序应当发布异常信息。可以将技术异常发布到一个由系统管理员或 Windows 管理规范 (WMI) 监视工具(如 Microsoft Operations Manager)监视的日志中;将业务异常发布到一个特定的应用程序日志中。通常,应允许从数据访问逻辑组件传播异常并允许由调用程序发布异常,以便您了解异常的整个环境。
以下示例说明了这些建议:
public class CustomerDALC
{
public void UpdateCustomer(Dataset aCustomer)
{
try
{
// 更新数据库中的客户...
}
catch (SqlException se)
{
// 捕获并包装异常,然后重新发出
throw new DataAccessException("数据库不可用", se);
}
finally
{
// 清除代码
}
}
}
|
在业务实体组件中管理异常的建议
业务实体组件应当向调用程序传播异常。在业务实体组件执行验证或者当调用程序试图执行某一操作而未提供该操作所需的数据时,业务实体组件也可以产生异常。
以下示例显示了业务实体组件如何产生异常。在此示例中,如果没有提供客户的名字,Update 方法将发出一个异常:
public class CustomerEntity
{
public void Update()
{
// 检查用户已提供了所需数据。这里是客户
// 的名字
if (FirstName == "" )
{
// 发出一个已定义的新的应用程序异常
throw new MyArgumentException("您必须提供名字。");
}
...
}
}
|
有关在 .NET 应用程序中处理异常的详细信息,请参阅 Exception Management in .NET。可以从 Exception Management Application Block 提供的 ApplicationException 类或 BaseApplicationException 类中继承自定义技术异常和自定义业务异常。
授权与安全性
学习向 Microsoft .NET 应用程序公开数据的最佳方式,以及如何实现一个有效的策略以便在分布式应用程序的层间传递数据。(本文包含一些指向英文站点的链接。)
本节说明如何将安全性应用于数据访问逻辑组件和业务实体组件。.NET 公共语言运行库使用权限对象实现其对托
文章来源于领测软件测试网 https://www.ltesting.net/
TAG:
net
设计
数据
应用
与