2004年11月27日

來源:MSDN

Angela Crocker、Andy Olsen 和 Edward Jezierski
Microsoft Corporation
2002年8月


适用于:
    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:Address



Customer:Order
一个客户可以有多个地址,例如送货地址、帐单接收地址、联系地址等。

一个客户可以有多个订单。
多对多 Order:Product 一个订单可以包含许多产品,每种产品由 OrderDetails 表中的单独一行表示。同样,一种产品也可以出现在许多订单中。

当定义业务实体以在数据库中建立信息模型时,应考虑要如何在您的应用程序中使用这些信息。应当标识封装您的应用程序的功能的核心业务实体,而不是为每个表定义单独的业务实体。


该假想零售商的应用程序中的典型操作如下:


  • 获取(或更新)客户的有关信息(包括地址)
  • 获取客户的订单列表
  • 获取特定订单的订购项目列表
  • 创建新订单
  • 获取(或更新)一个或一组产品的有关信息

为满足这些应用程序要求,该应用程序要处理三个逻辑业务实体: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> 元素。有关运行时如何定位程序集的信息,请参阅 How the Runtime Locates Assemblies
  • 序列化。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 值。
  • 在方法签名中使用自动兼容的数据类型。

有关 COM 互操作性的详细信息,请参阅 Microsoft .NET/COM Migration and Interoperability 指南。


实现业务实体


业务实体具有以下特点:


  • 业务实体提供对业务数据及相关功能(在某些设计中)的状态编程访问。
  • 业务实体可以使用具有复杂架构的数据来构建。这种数据通常来自数据库中的多个相关表。
  • 业务实体数据可以作为业务过程的部分 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 相关联的过程可能会减弱这种性能优势。

有关详细信息或性能方面的考虑,请参阅 Performance Comparison:Data Access Techniques


将业务实体表示为 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 中的表、约束及关系。有关详细信息,请参阅 Creating and Using DataSets


将业务实体表示为通用 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:自定义业务实体组件的作用(单击缩略图以查看大图像)


定义自定义业务实体组件的建议


在实现自定义实体组件时,请考虑以下建议:


  • 选择使用结构还是使用类。对于不包含分层数据或集合的简单业务实体,可以考虑定义一个结构来表示业务实体。对于复杂的业务实体或要求继承的业务实体,可将实体定义为类。有关结构和类这两种类型的比较,请参阅 Structures and Classes
  • 表示业务实体的状态。对于数字、字符串等简单值,可以使用等价的 .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 类添加声明属性以便在运行时指定对象的事务性要求。使用这种模型可以方便地配置多个组件以执行同一事务中的工作。

本节提供一些指导原则和建议,帮助您在数据访问逻辑组件和业务实体组件中实现事务处理支持。有关 .NET 中的事务处理示例和更深入的讨论,请参阅 .NET Data Access Architecture Guide


实现事务处理


在大多数环境中,事务处理的根本是业务过程而不是数据访问逻辑组件或业务实体组件。这是因为业务过程一般要求事务处理跨多个业务实体而不仅仅是单个业务实体。


然而,也可能出现在没有高层次业务过程的帮助下对单个业务实体执行事务性操作的情况。例如,要把一个新客户添加到前面讨论的数据库中,您必须执行以下操作:


  • 在 Customer 表中插入新的一行。
  • 在 Address 表中插入新的一行或多行。

只有这两个操作都成功后客户才会被添加到数据库中。如果 Customer 业务实体不会成为启动该事务处理的更大的业务过程的一部分,则应在 Customer 业务实体中使用手动事务处理。手动事务处理不要求与 Microsoft 分布式事务处理协调器 (DTC) 之间进行任何进程间通信,因此比自动事务处理要快得多。


图 11 所示为确定使用手动事务处理还是自动事务处理的方法。由于 COM+ 事务处理的系统开销,建议将事务处理放到数据库中并在存储过程中控制事务性行为(如果可能)。



图 11:确定如何实现事务处理


注意:如果从基于 ASP.NET 的客户端进行调用,并且没有用于启动事务处理的业务过程,则您可能会从 ASP.NET 代码中启动该事务处理。这种设计并不好;您决不能从基于 ASP.NET 的客户端启动事务处理,而应将数据的展示与业务过程相分离。此外,由于网络滞后等问题还会导致性能问题,因为这是要实际部署在其他层上的最常见的层。

在数据访问逻辑组件中使用手动事务处理的建议


在数据访问逻辑组件中实现手动事务处理时,请考虑以下建议:


  • 尽可能在存储过程中执行处理。使用 Transact-SQL 语句 BEGIN TRANSACTION、END TRANSACTION 和 ROLLBACK TRANSACTION 控制事务处理。有关代码示例,请参阅 .NET Data Access Architecture Guide 中的“How To Perform Transactions With Transact-SQL”。
  • 如果没有使用存储过程,并且也不会从业务过程中调用数据访问逻辑组件,则可以使用 ADO.NET 来编程控制事务处理。有关代码示例,请参阅 .NET Data Access Architecture Guide 中的“How to Code ADO.NET Manual Transactions”。

在数据访问逻辑组件中使用自动事务处理的建议


虽然 COM+ 事务处理会带来一些系统开销,但自动事务处理能够提供比手动事务处理更简单的编程模式,而且在事务处理跨多个分布式数据源(与 DTC 一起工作)时必须使用自动事务处理。在数据访问逻辑组件中实现自动事务处理时,请考虑以下建议:


  • 数据访问逻辑组件必须是从 System.EnterpriseServices 命名空间中的 ServicedComponent 类继承而来。注意,使用 COM+ 服务注册的所有程序集都必须具有严格的名称。有关严格命名的程序集的详细信息,请参阅 Creating and Using Strong-Named Assemblies
  • 使用 Transaction(TransactionOption.Supported) 属性注释数据访问逻辑组件,以便可以在同一组件中执行读写操作。与 Transaction(TransactionOption.Required) 不同,此选项在不需要事务处理时避免了不必要的系统开销,而前者始终会要求事务处理。

以下代码示例显示了如何在数据访问逻辑组件类中支持自动事务处理:

using System.EnterpriseServices;

[Transaction(TransactionOption.Supported)]
public class CustomerDALC : ServicedComponent
{

}


如果使用自动事务处理,则数据访问逻辑组件应在事务处理中表明操作是否成功。如果要隐式表明,应使用 AutoComplete 属性注释您的方法并在操作失败时发出一个异常。如果要显式表明,应对 ContextUtil 类调用 SetComplete 或 SetAbort 方法。


有关自动事务处理的详细信息,请参阅 .NET Data Access Architecture Guide 中的“Using Automatic Transactions”。


在业务实体组件中使用自动事务处理


在实现带有行为的自定义业务实体组件时,可以使用自动事务处理来指定这些对象的事务性行为。有关使用自动事务处理指定业务实体组件事务性行为的建议与前述有关在数据访问逻辑组件中实现自动事务处理的建议相同。


注意:如果业务实体组件不包含任何要求其在事务处理中表明是否成功的业务逻辑,则它可以忽略事务处理环境。自定义业务实体组件不需要从 ServicedComponent 继承;事务处理环境仍将继续其流程,但实体组件将忽略事务处理环境。

验证


您可以在应用程序的许多层上进行数据验证。各层适用不同的验证类型:


  • 在提交数据之前,客户端应用程序可以在本地验证业务实体数据。
  • 使用 XSD 架构接收业务文档时,业务过程可以验证这些文档。
  • 数据访问逻辑组件和存储过程可以验证数据,以确保引用的完整性并强制遵循约束以及重要的业务规则。

常用验证有两种:


  • 即时点验证。这是在一个特定时点执行的验证。例如,在接收 XML 文档时由业务过程对其进行验证。
  • 连续验证。这是在应用程序的许多不同层次上持续进行的一种验证。连续验证的示例包括:

    • 用户界面可以指定字段的最大长度以防止用户输入过长的字符串。
    • DataSet 可以指定数据列的最大长度。
    • 自定义业务实体组件可以对实体数据执行范围检查、长度检查、非空检查以及其他简单测试。
    • 数据访问逻辑组件、存储过程和数据库本身可以执行类似的测试,以便在将数据保存到数据库之前确保其有效性。

有时,您可能希望实现额外的聚合过程或转换过程。这种方法在验证和转换经常变化时可能很有用,但会损失性能。例如,如果一个 ISV 想要使用相同的组件支持数据库架构的两个版本,则您可以创建一个单独的组件来执行两个数据库架构版本之间的验证和转换。


如何使用 XSD 架构验证 XML


要使用 XSD 架构验证 XML 文档,请执行以下步骤:


  1. 创建一个 XmlValidatingReader 对象作为 XmlTextReader 对象的包装,如以下代码所示:
    ‘ 创建 XmlValidatingReader 对象,以读取和验证 Product.xml
    XmlTextReader tr = new XmlTextReader(“Product.xml”);
    XmlValidatingReader vr = new XmlValidatingReader(tr);

  2. 通过使用 ValidationType 枚举指定所需的验证类型。.NET Framework 支持三种类型的 XML 验证:

    • 文档类型定义 (DTD);指定 ValidationType.DTD
    • Microsoft XML 精简数据 (XDR) 架构;指定 ValidationType.XDR
    • W3C 标准 XSD 架构;指定 ValidationType.Schema

      以下代码显示了 ValidationType 枚举的使用:

      vr.ValidationType = ValidationType.Schema; ‘ 指定 XSD 架构验证

  3. 注册一个验证事件处理程序方法,如以下代码所示:
    vr.ValidationEventHandler += new ValidationEventHandler(MyHandlerMethod);

  4. 提供一个验证事件处理程序方法的实现,如以下代码所示:
    public void MyHandlerMethod(object sender, ValidationEventArgs e)
    {
    Console.WriteLine(“验证错误:” + e.Message);
    }

  5. 读取和验证文档,如以下代码所示。验证错误将被验证事件处理程序方法拾取。
    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 类中继承自定义技术异常和自定义业务异常。


授权与安全性


本节说明如何将安全性应用于数据访问逻辑组件和业务实体组件。.NET 公共语言运行库使用权限对象实现其对托管代码的强制限制机制。权限对象有三种,各自具有特定的用途:


  • 代码访问安全性。这些权限对象用于防止未经授权使用资源和操作。
  • 身份标识。这些权限对象指定运行程序集时所必需的身份标识特征。
  • 基于角色的安全性。这些权限对象提供了一个机制,用于判断用户(或用户的代理人)是否具有特定身份标识,或者是否是指定角色的成员。PrincipalPermission 对象是唯一基于角色的安全性权限对象。

托管代码可以使用 Principal 对象(包含对 Identity 对象的引用)来判断当事人的身份标识或角色。把 Identity 对象和 Principal 对象与用户、组帐户等大家所熟悉的概念比较可能会更容易理解。在 .NET Framework 中,Identity 对象表示用户,而角色表示成员身份和安全性环境。Principal 对象封装了 Identity 对象和角色。.NET Framework 中的应用程序根据 Principal 对象的身份标识或角色成员身份(后者更常见)授予 Principal 对象权限。


有关 .NET 中的权限与安全性的详细信息,请参阅 Key Security Concepts


数据访问逻辑组件中的安全性建议


数据访问逻辑组件的设计目的是供其他应用程序组件使用,它也是您的应用程序代码中在调用程序可以访问数据之前实现安全性的最后一个地方。


通常,数据访问逻辑组件可以依赖于由调用程序设置的安全性环境。然而,有些情况下数据访问逻辑组件必须执行自己的授权检查,以确定是否允许当事人执行所请求的操作。授权在身份验证后进行,并使用当事人身份标识与角色等有关信息来确定该当事人可以访问的资源。


在以下情况下,应在数据访问逻辑组件层次上执行授权检查:


  • 需要与不完全信任的业务过程开发人员共享数据访问逻辑组件
  • 需要保护对数据存储提供的强大功能的访问

在定义了 Identity 对象和 Principal 对象后,可以用三种方式执行基于角色的安全性检查:


  • 使用 PrincipalPermission 对象执行强制性安全性检查。
  • 使用 PrincipalPermissionAttribute 属性执行说明性安全性检查。
  • 使用 Principal 对象中的属性和 IsInRole 方法执行显式安全性检查。

以下代码示例显示了如何使用 PrincipalPermissionAttribute 为数据访问逻辑组件类中的方法指定基于角色的声明性安全性检查:

using System;
using System.Security.Permissions;

public class CustomerDALC
{

public CustomerDALC()
{
}

// 使用 PrincipalPermissionAttribute 要求此方法的调用程序
// 具有一个名为“MyUser”的身份标识,并且属于角色“Administrator”。
[PrincipalPermissionAttribute(SecurityAction.Demand,
Name="MyUser", Role="Administrator")]
public void DeleteCustomer(string customerID)
{
// 在此处删除客户代码
}
}


以下代码显示了如何创建具有所需身份标识和角色的 Principal 对象,以便对 CustomerDALC 对象调用 DeleteCustomer 方法:

using System;
using System.Security.Principal;
using System.Threading;

public class MainClass
{
public static int Main(string[] args)
{
Console.Write(“用户名:”);
string UserName = Console.ReadLine();

Console.Write(“密码:”);
string Password = Console.ReadLine();

if (Password == “password” && UserName == “MyUser”)
{
// 创建一个名为“MyUser”的通用身份标识
GenericIdentity MyIdentity = new GenericIdentity(“MyUser”);

// 创建角色
String[] MyString = {“Administrator”, “User”};

// 创建一个通用当事人
GenericPrincipal MyPrincipal = new GenericPrincipal(MyIdentity,
MyString);

// 设置此线程的当前当事人,以用于基于角色的安全性
Thread.CurrentPrincipal = MyPrincipal;
}

// 创建一个 CustomerDALC 对象,并尝试调用它的 DeleteCustomer 方法。
// 仅在当前当事人的身份标识和角色合格时这一步骤才能成功。
CustomerDALC c = new CustomerDALC();
c.DeleteCustomer(“VINET”);
}
}


Windows 身份验证


理想情况下,在连接到数据库时应使用 Windows 身份验证而不是 SQL Server 身份验证。然而,应使用服务帐户并避免模拟连接到数据库,因为它会妨碍连接池。连接池需要相同的连接字符串;如果尝试使用不同的连接字符串打开数据库,就会创建单独的连接池,而这将限制可缩放性。


有关 Windows 身份验证和连接池的详细信息,请参阅 .NET Data Access Architecture Guide 中的“Managing Database Connections”。


安全通信建议


要实现调用应用程序与数据访问逻辑组件之间的安全通信,请考虑以下建议:


  • 如果数据访问逻辑组件是通过各种层的线路调用的,并且信息交换包含需要保护的机密信息,则应使用分布式组件对象模型 (DCOM)、安全套接字层 (SSL)、安全 Internet 协议 (IPSec) 等安全通信技术。
  • 如果数据是加密存储在数据库中,则通常由数据访问逻辑组件负责数据的加密与解密。如果信息暴露会导致巨大损害,则必须考虑保护与数据访问逻辑组件进行通信的通道。

业务实体组件中的安全性建议


如果将业务实体实现为数据结构(如 XML 或 DataSet),则不需要实现安全性检查。然而,如果将业务实体实现为带有 CRUD 操作的自定义业务实体组件,请考虑以下建议:


  • 如果将实体提供给您不完全信任的业务过程,应在业务实体组件和数据访问逻辑组件中实现授权检查。然而,如果在这两个层次上都实现检查,可能会产生保持安全性策略同步的维护问题。
  • 业务实体组件不应处理通信安全性或数据加密,应把这些任务留给相应的数据访问逻辑组件。

部署


本节提供一些建议以帮助您确定如何部署数据访问逻辑组件和业务实体组件。


部署数据访问逻辑组件


部署数据访问逻辑组件的方法有两种:


  • 与业务过程对象一起部署数据访问逻辑组件。这种部署方法具有最佳的数据传输性能,还有一些额外的技术优势:

    • 事务处理可以在业务过程对象和数据访问逻辑组件之间无缝流动。然而,事务处理不能跨越远程通道无缝流动。在远程方案下,需要使用 DCOM 来实现事务处理。此外,如果业务过程与数据访问逻辑组件被防火墙分开,还需要打开这两个物理层之间的防火墙端口以启用 DTC 通信。
    • 一起部署业务过程对象和数据访问逻辑组件可以减少事务处理失败节点的数目。
    • 安全性环境自动在业务过程对象和数据访问逻辑组件之间流动,无需设置当事人对象。

  • 与用户界面代码一起部署数据访问逻辑组件。有时需要直接从 UI 组件和 UI 过程组件使用数据访问逻辑组件。为提高 Web 方案下的性能,可以与 UI 代码一起部署数据访问逻辑组件;这种部署方法可以使 UI 层充分利用数据读取器流以获得最佳性能。然而,在使用这种部署方法时必须牢记以下事项:

    • 不与 UI 代码一起部署数据访问逻辑组件的一个常见原因是防止通过 Web 领域直接对数据源进行网络访问。
    • 如果您的 Web 领域部署在 DMZ 环境中,则必须打开防火墙端口才能访问 SQL Server。如果使用 COM+ 事务处理,还必须为 DTC 通信打开其他的防火墙端口。有关详细信息,请参阅 .NET Data Access Architecture Guide

部署业务实体


应用程序的许多不同层都要使用业务实体。根据业务实体的实现方式,如果您的应用程序跨越各个物理层,则需要将业务实体部署到多个位置。下面列出了在不同实现方案中部署业务实体的方法:


  • 部署作为有类型的 DataSet 实现的业务实体。有类型的 DataSet 类必须由数据访问逻辑组件和调用应用程序访问。因此,建议在一个要部署在多个层的公共程序集中定义有类型的 DataSet 类。
  • 部署作为自定义业务实体组件实现的业务实体。根据数据访问逻辑组件中定义的方法签名,自定义实体类可能需要由数据访问逻辑组件访问。请遵循与有类型的 DataSet 相同的建议,即在一个要部署在多个层的公共程序集中定义自定义实体类。
  • 部署作为通用 DataSet 或 XML 字符串实现的业务实体。通用 DataSet 和 XML 字符串不表示单独的数据类型。以这两种格式实现的业务实体不存在部署问题。

附录


如何定义数据访问逻辑组件类
如何使用 XML 表示数据的集合和层次结构
如何在 .NET 应用程序中编程应用样式表
如何创建有类型的 DataSet
如何定义业务实体组件
如何表示业务实体组件中数据的集合和层次结构
如何将业务实体组件绑定到用户界面控件
如何在业务实体组件中提供事件
如何将业务实体组件序列化为 XML 格式
如何将业务实体组件序列化为 SOAP 格式
如何将业务实体组件序列化为二进制格式


如何定义数据访问逻辑组件类


以下代码示例定义一个名为 CustomerDALC 的类,它是用于 Customer 业务实体的数据访问逻辑组件类。CustomerDALC 类为 Customer 业务实体实现 CRUD 操作,并提供了其他方法为此对象封装业务逻辑。

public class CustomerDALC
{
private string conn_string;

public CustomerDALC()
{
// 从安全或加密的位置获取连接字符串
// 并将其分配给 conn_string
}

public CustomerDataSet GetCustomer(string id)
{
// 检索包含 Customer 数据的有类型的 DataSet
}

public string CreateCustomer(string name,
string address, string city, string state,
string zip)
{
// 根据传递给此方法的标量参数,在数据库中创建一个
// 新客户。
// 从此方法返回 customerID。
}

public void UpdateCustomer(CustomerDataSet updatedCustomer)
{
// 根据作为类型 CustomerDataSet 的参数发送的 Customer 数据,更新
// 数据库。
}

public void DeleteCustomer(string id)
{
// 删除具有指定 ID 的客户
}

public DataSet GetCustomersWhoPurchasedProduct(int productID)
{
// 使用通用 DataSet 检索客户,因为此方法
// 不需要检索与客户关联的全部信息
}
}


如何使用 XML 表示数据的集合和层次结构


以下示例显示了如何在 XML 文档中表示数据的集合和层次结构。该 XML 文档表示客户的一个订单;注意,元素 <OrderDetails> 包含一个该订单的详细信息集合。

<Order xmlns=”urn:aUniqueNamespace”>
<OrderID>10248</OrderID>
<CustomerID>VINET</CustomerID>
<OrderDate>1996-07-04</OrderDate>
<ShippedDate>1996-07-16</ShippedDate>
<OrderDetails>
<OrderDetail>
<ProductID>11</ProductID>
<UnitPrice>14.00</UnitPrice>
<Quantity>12</Quantity>
</OrderDetail>
<OrderDetail>
<ProductID>42</ProductID>
<UnitPrice>9.80</UnitPrice>
<Quantity>10</Quantity>
</OrderDetail>
<OrderDetail>
<ProductID>72</ProductID>
<UnitPrice>34.80</UnitPrice>
<Quantity>5</Quantity>
</OrderDetail>
</OrderDetails>
</Order>

如何在 .NET 应用程序中编程应用样式表


要在 .NET 应用程序中编程应用样式表,请执行以下步骤:


  1. 导入 System.Xml.Xsl 命名空间,如以下代码所示。System.Xml.Xsl 命名空间包含 .NET Framework 类库中的 XSLT 转换类。
    using System.Xml.Xsl;

  2. 创建一个 XslTransform 对象,如以下代码所示:
    XslTransform stylesheet = new XslTransform();

  3. 将所需样式表加载到 XslTransform 对象,如以下代码所示:
    stylesheet.Load(“MyStylesheet.xsl”);

  4. 调用 XslTransform 对象的 Transform 方法,如以下代码所示。调用 Transform 方法指定 XML 源文档和结果文档的名称。
    stylesheet.Transform(sourceDoc, resultDoc);

如何创建有类型的 DataSet


可以使用有类型的 DataSet 表示业务实体。创建有类型的 DataSet 的方法有多种:


  • 从 Microsoft Visual Studio ®.NET 中的数据适配器创建
  • 从 Visual Studio .NET 中的 XSD 架构文件创建
  • 使用 XSD 架构定义工具 (xsd.exe) 从 .NET Framework 命令提示窗口创建

注意:也可以编程定义有类型的 DataSet,即从 DataSet 继承并定义方法、属性和嵌套类以表示该 DataSet 的结构。最简单的方法是使用以下过程之一创建一个有类型的 DataSet,然后将此有类型的 DataSet 类用作将来您自己的有类型的 DataSet 类的基础。

使用数据适配器创建有类型的 DataSet


要使用数据适配器创建有类型的 DataSet,请执行以下步骤:


  1. 在 Visual Studio .NET 中,向您的窗体或组件添加一个数据适配器。在数据适配器的配置向导中,指定该数据适配器的连接信息。同时根据具体情况,为数据适配器的 Select、Insert、Update 和 Delete 命令指定 SQL 字符串或存储过程。
  2. 在组件设计器中,在数据适配器对象上单击鼠标右键,然后单击 Generate DataSet(生成 DataSet)。
  3. 在 Generate DataSet(生成 DataSet)对话框中,单击 New(新建),键入新 DataSet 类的名称,然后单击 OK(确定)。
  4. 为确认已创建该有类型的 DataSet,可以在解决方案资源管理器中单击 Show All Files(显示所有文件)按钮。展开 XSD 架构文件的节点,确认存在一个与 XSD 架构相关联的代码文件。该代码文件定义了新的有类型的 DataSet 类。

从 XSD 架构文件创建有类型的 DataSet


要使用 Visual Studio .NET 从 XSD 架构文件创建有类型的 DataSet,请执行以下步骤:


  1. 在 Visual Studio .NET中,创建一个新项目或打开一个现有项目。
  2. 为项目添加一个现有的 XSD 架构,或在组件设计器中创建一个新的 XSD 架构。
  3. 在解决方案资源管理器中,双击 XSD 架构文件,在组件设计器中查看该 XSD 架构。
  4. 在组件设计器中选择主 XSD 架构元素。
  5. 在 Schema(架构)菜单中,单击 Generate DataSet(生成 DataSet)。
  6. 为确认已创建该有类型的 DataSet,可以在解决方案资源管理器中单击 Show All Files(显示所有文件)按钮。展开 XSD 架构文件的节点,确认存在一个与 XSD 架构相关联的代码文件。该代码文件定义了新的有类型的 DataSet 类。

使用 XSD 架构定义工具 (xsd.exe) 创建有类型的 DataSet


XML 架构定义工具可以从 XSD 架构文件、XDR 架构文件或 XML 实例文档生成有类型的 DataSet。以下命令使用名为 XsdSchemaFile.xsd 的 XSD 架构文件,在当前目录中名为 XsdSchemaFile.cs 的 Visual C# 源文件中生成一个有类型的 DataSet:

xsd /dataset /language:C# XsdSchemaFile.xsd

有关详细信息,请参阅 Generating a Strongly Typed DataSet


如何定义业务实体组件


以下示例显示了如何为 Product 业务实体定义自定义实体类:

public class ProductEntity
{
// 专用字段,用于保存 Product 实体的状态
private int productID;
private string productName;
private string quantityPerUnit;
private decimal unitPrice;
private short unitsInStock;
private short unitsOnOrder;
private short reorderLevel;

// 公共属性,用于公开 Product 实体的状态
public int ProductID
{
get { return productID; }
set { productID = value; }
}
public string ProductName
{
get { return productName; }
set { productName = value; }
}
public string QuantityPerUnit
{
get { return quantityPerUnit; }
set { quantityPerUnit = value; }
}
public decimal UnitPrice
{
get { return unitPrice; }
set { unitPrice = value; }
}
public short UnitsInStock
{
get { return unitsInStock; }
set { unitsInStock = value; }
}
public short UnitsOnOrder
{
get { return unitsOnOrder; }
set { unitsOnOrder = value; }
}
public short ReorderLevel
{
get { return reorderLevel; }
set { reorderLevel = value; }
}

// 执行本地化处理的方法和属性
public void IncreaseUnitPriceBy(decimal amount)
{
unitPrice += amount;
}
public short UnitsAboveReorderLevel
{
get { return (short)(unitsInStock – reorderLevel); }
}
public string StockStatus
{
get
{
return “库存:”+ unitsInStock + “,订购:” + unitsOnOrder;
}
}
}


如何表示业务实体组件中数据的集合和层次结构


以下示例显示了如何为 Order 业务实体定义自定义实体类。每个订单都包含许多订购项目,这些订购项目保存在 OrderEntity 类的一个 DataSet 中。

public class OrderEntity
{
// 专用字段,用于保存订单信息
private int orderID;
private string customerID;
private DateTime orderDate;
private DateTime shippedDate;

// 专用字段,用于保存订单详细信息
private DataSet orderDetails;

// 公共属性,用于提供订单信息
public int OrderID
{
get { return orderID; }
set { orderID = value; }
}
public string CustomerID
{
get { return customerID; }
set { customerID = value; }
}
public DateTime OrderDate
{
get { return orderDate; }
set { orderDate = value; }
}
public DateTime ShippedDate
{
get { return shippedDate; }
set { shippedDate = value; }
}

// 公共属性,用于提供订单详细信息
public DataSet OrderDetails
{
get { return orderDetails; }
set { orderDetails = value; }
}

// 附加方法,用于简化对订单详细信息的访问
public bool IsProductOrdered(int productID)
{
// 必须在 DataTable 中定义主关键字列
DataRow row = orderDetails.Tables[0].Rows.Find(productID);

if (row != null)
return true;
else
return false;
}

// 附加属性,用于简化对订单详细信息的访问
public int NumberOfOrderItems
{
get
{
return orderDetails.Tables[0].Rows.Count;
}
}
}


关于 OrderEntity 类,请注意以下几点:


  • 该类包含用于保存有关订单的信息的专用字段。还有一个专用 DataSet 字段,用于保存订单的其他详细信息。数据访问逻辑组件将在创建 OrderEntity 对象时填充所有这些字段。
  • 该类包含用于提供有关订单的信息的公共属性。此外还有一个用于提供该 DataSet 的属性,以便使调用应用程序能够访问订单详细信息。
  • 该类包含一个附加方法和一个附加属性,用于简化对订单详细信息的访问:

    • IsProductOrdered 方法接收一个 ProductID 参数,并返回一个布尔值以表明该产品是否出现在订单中。
    • NumberOfOrderItems 属性表明订单中的订购行数目。

如何将业务实体组件绑定到用户界面控件


可以将用户界面控件绑定到 Windows 窗体和 ASP.NET 应用程序中的自定义实体。有两种可能的方案:


  • 在用户界面控件上绑定单个业务实体。以下代码示例显示了如何从 OrderDALC 对象获取一个 OrderEntity 对象并将其绑定到 Windows 窗体的控件上。当用户更改这些控件中的值时,基础 OrderEntity 对象中的数据也将自动更改。
    // 创建 OrderDALC 对象。
    OrderDALC dalcOrder = new OrderDALC();

    // 使用 dalcOrder 为订单 ID 10248 获取一个 OrderEntity 对象。
    // 此代码假设 OrderDALC 类有一个名为 GetOrder() 的方法,
    // 该方法为特定订单 ID 返回一个 OrderEntity 对象。
    OrderEntity order = dalcOrder.GetOrder(10248);

    // 将 OrderEntity 的 OrderID 属性绑定到 TextBox 控件。
    textBox1.DataBindings.Add(“Text”, order, “OrderID”);

    // 将 OrderEntity 的 CustomerID 属性绑定到另一个 TextBox 控件。
    control.
    textBox2.DataBindings.Add(“Text”, order, “CustomerID”);

    // 将 OrderEntity 的 OrderDate 属性绑定到 DatePicker 控件。
    dateTimePicker1.DataBindings.Add(“Value”, order, “OrderDate”);

    // 将 OrderEntity 的 ShippedDate 属性绑定到另一个 DatePicker 控件。
    dateTimePicker2.DataBindings.Add(“Value”, order, “ShippedDate”);

    // 将 OrderEntity 的 OrderDetails DataSet 绑定到 DataGrid 控件。
    // DataGrid 分别用网格中的一行显示 DataSet 的各个 DataRow。
    dataGrid1.DataSource = order.OrderDetails.Tables[0].DefaultView;


    准备好后,您可以将修改后的 OrderEntity 对象传递给 OrderDALC,以便将数据保存到数据库中,如以下代码所示。

    // 通过 dalcOrder 将 OrderEntity 对象保存到数据库中。
    // 此代码假设 OrderDALC 类有一个名为 UpdateOrder() 的方法,
    // 该方法接收一个 OrderEntity 参数,并更新数据库中的相应项
    dalcOrder.UpdateOrder(order);

  • 将业务实体集合绑定到 DataGrid 控件。以下代码示例显示了如何从 OrderDALC 获取一个 OrderEntity 对象数组并将其绑定到 Windows 窗体的 DataGrid 控件。DataGrid 分别用网格中的一行显示每个数组元素(即每个 OrderEntity 对象)。
    // 创建 OrderDALC 对象。
    OrderDALC dalcOrder = new OrderDALC();

    // 使用 dalcOrder 获取客户“VINET”的 OrderEntity 对象数组。
    // 此代码假设 OrderDALC 类有一个名为
    GetOrdersForCustomer(),
    // 的方法,该方法返回特定客户的 OrderEntity 对象数组。
    OrderEntity[] orderEntities = dalcOrder.GetOrdersForCustomer(“VINET”);

    // 将该数组绑定到 DataGrid 控件。
    dataGrid1.DataSource = orderEntities;


    准备好后,您可以将修改后的数组传递给 OrderDALC,以便将数据保存到数据库中,如以下代码所示:

    // 通过 dalcOrder 将 OrderEntity 对象保存到数据库中。
    // 此代码假设 OrderDALC 类有一个名为 UpdateOrder() 的方法,该方法获取
    // 一个 OrderEntity 对象数组,并更新数据库中的相应项。
    dalcOrder.UpdateOrders(orderEntities);

如何在业务实体组件中提供事件


自定义实体可以在业务实体状态修改时产生事件。这些事件可用于获得丰富的客户端用户界面设计,因为这使得无论数据显示在哪里都可以对其进行刷新。以下代码示例显示了如何在 OrderEntity 类中产生业务实体相关事件:

// 为所有业务实体事件定义公用事件类
public class EntityEventArgs : EventArgs
{
// 定义事件成员,用于提供有关事件的信息
}

// 定义一个代理,用于为业务实体相关事件指定签名
public delegate void EntityEventHandler(Object source, EntityEventArgs e);

// 定义自定义实体类,它可以在业务实体状态改变时产生事件
public class OrderEntity
{
// 定义业务实体状态改变的“before”事件和“after”事件
public event EntityEventHandler BeforeChange, AfterChange;

// 专用字段,用于保存业务实体的状态
private int orderID;
private int customerID;
private DateTime orderDate;
private DateTime shippedDate;
private DataSet orderDetails;

// 公共属性,用于提供业务实体的状态
public int OrderID
{
get { return orderID; }
set
{
BeforeChange(this, new EntityEventArgs()); // 产生“before”事件
orderID = value;
AfterChange(this, new EntityEventArgs()); // 产生“after”事件
}
}
public int CustomerID
{
get { return customerID; }
set
{
BeforeChange(this, new EntityEventArgs()); // 产生“before”事件
customerID = value;
AfterChange(this, new EntityEventArgs()); // 产生“after”事件
}
}
public DateTime OrderDate
{
get { return orderDate; }
set
{
BeforeChange(this, new EntityEventArgs()); // 产生“before”事件
orderDate = value;
AfterChange(this, new EntityEventArgs()); // 产生“after”事件
}
}
public DateTime ShippedDate
{
get { return shippedDate; }
set
{
BeforeChange(this, new EntityEventArgs()); // 产生“before”事件
shippedDate = value;
AfterChange(this, new EntityEventArgs()); // 产生“after”事件
}
}

// 必要时使用更多成员…
}


关于上述代码,请注意以下几点:


  • EntityEvent 类提供有关业务实体相关事件的信息。EntityEventHandler 代理为自定义实体类产生的所有业务实体相关事件指定签名。该代理签名遵循所建议的 .NET Framework 事件处理程序代理的原则。有关在 .NET 中定义和使用事件的原则,请参阅 Event Usage Guidelines
  • OrderEntity 类定义了两个名为 BeforeChange 和 AfterChange 的事件。
  • OrderEntity 中的属性设置器在业务实体状态改变前产生一个 BeforeChange 事件,在业务实体状态改变后产生一个 AfterChange 事件。

如何将业务实体组件序列化为 XML 格式


本节讨论以下问题:


  • 使用 XmlSerializer 序列化自定义实体对象
  • XML Web services 中对象的 XML 序列化
  • 序列化自定义实体对象的默认 XML 格式
  • 控制序列化自定义实体对象的 XML 格式

使用 XmlSerializer 序列化自定义实体对象


以下代码示例显示了如何使用 XmlSerializer 类将 OrderEntity 对象序列化为 XML 格式:

using System.Xml.Serialization;     // 此命名空间包含 XmlSerializer 类

// 创建一个 XmlSerializer 对象,用于序列化 OrderEntity 类型的对象
XmlSerializer serializer = new XmlSerializer(typeof(OrderEntity));

// 将 OrderEntity 对象序列化为名为“MyXmlOrderEntity.xml”的 XML 文件
TextWriter writer = new StreamWriter(“MyXmlOrderEntity.xml”);
serializer.Serialize(writer, order);
writer.Close();


在 XML Web services 中序列化对象


以下代码示例显示了如何编写使用自定义实体对象的 XML Web services:

namespace MyWebService
{
[WebService(Namespace="urn:MyWebServiceNamespace")]
public class OrderWS : System.Web.Services.WebService
{
[WebMethod]
public OrderEntity GetOrder(int orderID)
{
// 创建 OrderDALC 对象
OrderDALC dalcOrder = new OrderDALC();

// 使用 dalcOrder 获取指定订单 ID 的 OrderEntity 对象。
// 此代码假设 OrderDALC 类有一个名为 GetOrder 的方法,
// 该方法获取一个订单 ID 作为参数,并返回一个 OrderEntity 对象,
// 其中包含该订单的所有数据。
OrderEntity order = dalcOrder.GetOrder(10248);

// 返回 OrderEntity 对象, 该对象将自动序列化。
return order;
}

[WebMethod]
public void UpdateOrder(OrderEntity order)
{
// 创建 OrderDALC 对象。
OrderDALC dalcOrder = new OrderDALC();

// 使用 dalcOrder 将 OrderEntity 对象的数据保存到数据库中。
// 此代码假设 OrderDALC 类有一个名为 UpdateOrder 的方法,
// 该方法接收一个 OrderEntity 对象并将数据保存到数据库中。
dalcOrder.UpdateOrder(order);
}


关于上述代码,请注意以下几点:


  • GetOrder 方法接收一个订单 ID 作为参数,并返回包含该订单的数据的 OrderEntity 对象。
  • UpdateOrder 方法接收一个 OrderEntity 对象并将该对象的数据保存到数据库中。
  • 如果客户端应用程序调用 GetOrder 和 UpdateOrder 方法,OrderEntity 对象将为该方法调用自动序列化为 XML 格式。

序列化自定义实体对象的默认 XML 格式


以下 XML 文档显示了 OrderEntity 对象的默认 XML 序列化格式:

<?xml version=”1.0″ encoding=”utf-8″?>
<OrderEntity xmlns:xsd=”http://www.w3.org/2001/XMLSchema”
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”>
<OrderID>10248</OrderID>
<CustomerID>VINET</CustomerID>
<OrderDate>1996-07-04T00:00:00.0000000+01:00</OrderDate>
<OrderDetails> … see below … </OrderDetails>
<ShippedDate>1996-07-16T00:00:00.0000000+01:00</ShippedDate>
</OrderEntity>

上述文档说明了 XML 序列化的默认规则:


  • 该 XML 文档的根元素与类名称 OrderEntity 相同。
  • OrderEntity 对象中的每个公共属性(及字段)都被序列化为具有相同名称的元素。

OrderEntity 类中的 OrderDetails 属性是一个 DataSet,DataSet 提供了内置的 XML 序列化支持。OrderDetails DataSet 的序列化结果如下:

<OrderDetails>
<xs:schema id=”NewDataSet” xmlns=”"
xmlns:xs=”http://www.w3.org/2001/XMLSchema”
xmlns:msdata=”urn:schemas-microsoft-com:xml-msdata”>
<xs:element name=”NewDataSet” msdata:IsDataSet=”true” msdata:Locale=”en-
UK”>
<xs:complexType>
<xs:choice maxOccurs=”unbounded”>
<xs:element name=”OrderDetails”>
<xs:complexType>
<xs:sequence>
<xs:element name=”OrderID” type=”xs:int” minOccurs=”0″ />
<xs:element name=”ProductID” type=”xs:int” minOccurs=”0″ />
<xs:element name=”UnitPrice” type=”xs:decimal” minOccurs=”0″
/>
<xs:element name=”Quantity” type=”xs:short” minOccurs=”0″ />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
<diffgr:diffgram xmlns:msdata=”urn:schemas-microsoft-com:xml-msdata”
xmlns:diffgr=”urn:schemas-microsoft-com:xml-diffgram-v1″>
<NewDataSet>
<OrderDetails diffgr:id=”OrderDetails1″ msdata:rowOrder=”0″
diffgr:hasChanges=”inserted”>
<OrderID>10248</OrderID>
<ProductID>11</ProductID>
<UnitPrice>14</UnitPrice>
<Quantity>12</Quantity>
</OrderDetails>
<OrderDetails diffgr:id=”OrderDetails2″ msdata:rowOrder=”1″
diffgr:hasChanges=”inserted”>
<OrderID>10248</OrderID>
<ProductID>42</ProductID>
<UnitPrice>9.8</UnitPrice>
<Quantity>10</Quantity>
</OrderDetails>
<OrderDetails diffgr:id=”OrderDetails3″ msdata:rowOrder=”2″
diffgr:hasChanges=”inserted”>
<OrderID>10248</OrderID>
<ProductID>72</ProductID>
<UnitPrice>34.8</UnitPrice>
<Quantity>5</Quantity>
</OrderDetails>
</NewDataSet>
</diffgr:diffgram>
</OrderDetails>

关于 DataSet 的序列化,请注意以下几点:


  • <xs:schema> 段描述了 DataSet 的结构,包括表、列名称和列类型。
  • <xs:diffgram> 段包含该 DataSet 的数据。每个 <OrderDetails> 元素表示该 DataSet 中 OrderDetails 表中的单独一行。

控制序列化自定义实体对象的 XML 格式


您可以在自定义实体类中使用 .NET 属性来控制属性和字段序列化为 XML 的方式。请考虑以下修订后的 OrderEntity 类:

 [XmlRoot(ElementName="Order", Namespace="urn:MyNamespace")]
public class OrderEntity
{
[XmlAttribute(AttributeName="ID")]
public int OrderID {…获取和设置代码,同前…}

[XmlAttribute(AttributeName="CustID")]
public string CustomerID {…获取和设置代码,同前…}

[XmlElement(ElementName="Ordered")]
public DateTime OrderDate {…获取和设置代码,同前…}

public DataSet OrderDetails {…获取和设置代码,同前…}

[XmlElement(ElementName="Shipped")
public DateTime ShippedDate {...获取和设置代码,同前...}

// 必要时使用更多成员...
}



  • 将 OrderEntity 对象序列化为 XML 后,其格式如下:
<?xml version="1.0" encoding="utf-8" ?>
<Order ID="10248"
CustID="VINET"
xmlns="urn:MyNamespace"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Ordered>1996-07-04T00:00:00.0000000+01:00</Ordered>
<OrderDetails>...详细代码同前...</OrderDetails>
<Shipped>1996-07-16T00:00:00.0000000+01:00</Shipped>
</Order>

有关如何使用属性来控制 XML 序列化的详细信息,请参阅 Attributes that Control XML Serialization


如何将业务实体组件序列化为 SOAP 格式


以下代码示例显示了如何使用 SoapFormatter 类将 OrderEntity 对象序列化为 SOAP 格式。当使用 SOAP 协议向或从 XML Web services 传递对象,或者当使用 HTTP 远程通道向或从 Remoting 服务器传递对象时,也会发生 SOAP 序列化(隐式)。此外,您也可以在使用 TCP 远程通道时指定 SOAP 格式化。

using System.Runtime.Serialization.Formatters.Soap;    // 用于 SoapFormatter 类
...
// 创建 SoapFormatter 对象,用于序列化 OrderEntity 类型的对象
SoapFormatter formatter = new SoapFormatter();

// 将 OrderEntity 对象序列化为名为“MySoapOrderEntity.xml”的 SOAP (XML) 文件
FileStream stream = File.Create("MySoapOrderEntity.xml");
formatter.Serialize(stream, order);
stream.Close();


要对自定义实体组件使用 SOAP 序列化,必须使用 Serializable 属性注释您的实体类,如以下代码所示:

 [Serializable]
public class OrderEntity
{
// 成员,同前

如果要自定义序列化过程中生成的 SOAP 格式,实体类必须实现 ISerializable 接口。您必须提供一个 GetObjectData 方法供 SoapFormatter 在序列化过程中调用,并提供一个特殊构造函数供 SoapFormatter 在还原序列化过程中调用以重新创建对象。以下代码显示了 ISerializable 接口、GetObjectData 方法和特殊构造函数的使用:

using System.Runtime.Serialization;   // 用于 ISerializable 接口以及相关类型

[Serializable]
public class OrderEntity : ISerializable
{
// 序列化函数,由 SoapFormatter 在序列化过程中调用
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext
ctxt)
{
// 向 SerializationInfo 对象中添加每个字段
info.AddValue(“OrderID”, orderID);
// 必要时使用更多代码…
}

// 还原序列化构造函数,由 SoapFormatter 在还原序列化过程中调用
public OrderEntity(SerializationInfo info, StreamingContext ctxt)
{
// 从 SerializationInfo 对象中还原序列化出各个 OrderEntity 字段
orderID = (int)info.GetValue(“OrderID”, typeof(int));
// 必要时使用更多代码…
}

// 其他成员,同前…
}


有关自定义 SOAP 序列化的详细信息,请参阅 Basic Serialization


如何将业务实体组件序列化为二进制格式


以下代码示例显示了如何使用 BinaryFormatter 类将 OrderEntity 对象序列化为二进制格式。当使用 TCP 远程通道向或从 Remoting 服务器传递对象时,也会发生二进制序列化(隐式)。此外,为提高性能,您也可以在使用 HTTP 远程通道时指定二进制格式化。

using System.Runtime.Serialization.Formatters.Binary;    // 用于 BinaryFormatter 类

// 创建 BinaryFormatter 对象,用于序列化 OrderEntity 类型的对象
BinaryFormatter formatter = new BinaryFormatter();

// 将 OrderEntity 对象序列化为名为“MyBinaryOrderEntity.dat”的二进制文件
FileStream stream = File.Create(“MyBinaryOrderEntity.dat”);
formatter.Serialize(stream, order);
stream.Close();


要对自定义实体对象使用二进制序列化,必须使用 Serializable 属性注释您的自定义实体类。要自定义序列化过程中生成的二进制格式,自定义实体类必须实现 ISerializable 接口。这两种方案中的详细代码与 SOAP 序列化的代码相同。


有关二进制序列化的详细信息,请参阅 Binary Serialization

2004年10月20日

來源:MSDN

Dear Dr. GUI,

Microsoft has always kept the art of attaching an icon to a menu item to itself. I had solutions that worked with Visual Basic 6.0, but I need some insight on a Visual Basic .NET solution. Could you shed some light on this?

Thanks in advance,
Patrick Wimberley

Dr. GUI replies:

Luckily, the good doctor got plenty of rest last night and is full of energy to tackle the challenge you’re offering. So let’s take off our cardigan sweaters and get to work in the .NET lab, neighbor!

Unfortunately, the MenuItem class does not provide a built-in way of displaying an icon. For example, there is no Icon property you set to display an icon automatically.

But don’t despair; the .NET Framework is remarkably extensible, so if a Framework class doesn’t provide a feature you want, then you can usually extend that class through inheritance and a virtual function override or two, and provide the feature yourself—without having to write the whole class from scratch. This is old hat to C++ and Java programmers, but this kind of easy extensibility is a new feature of Microsoft? Visual Basic? .NET.

In this case, the MenuItem class, like many Windows Forms visible objects, provides the capability for non-standard drawing, called “owner-draw.” It does this by providing two virtual methods called OnMeasureItem and OnDrawItem. So let’s create an owner-drawn menu item, which gives you (the owner) the right (and responsibility) of drawing or painting the MenuItem on the screen. First, we create a new class and indicate that it inherits from the old class:

Public Class MyIconMenuItem Inherits MenuItem ' ... 

When you inherit from MenuItem, you inherit ALL of its properties, methods, and events. You want to override the behavior of two of the methods that you inherit so you can do your own drawing for your menu item class. These two methods are OnMeasureItem and OnDrawItem. Before examining the code for these methods, you’ll want to declare some private fields within your class to hold an Icon object and a Font object. You need a Font object because you want your menu item to be able to contain text as well as icons.

You also need a way for developers using your class to initialize your private fields. You can do this through your class’s constructor. A Visual Basic .NET constructor is a special method called New that gets called by the runtime when an object of your class is created. (You can think of it as being similar to the Class_Initialize event in Visual Basic 6.0.) With Visual Basic .NET, you can actually pass parameters to your constructor!

So let’s take the concept of constructors and use it in your class to allow the developers who use your class to initialize your class’s private fields. Your custom menu item class should now look something like this:

Public Class MyIconMenuItem Inherits MenuItem Private font As Font Private icon As Icon Sub New(ByVal menuText As String, ByVal handler As EventHandler, _ ByVal shortcut As Shortcut, ByVal ico As icon) MyBase.New(menuText, handler, shortcut) Me.icon = ico Me.font = SystemInformation.MenuFont Me.OwnerDraw = True End Sub ' ... 

What if you don’t want to have an icon? Just use a MenuItem rather than a MyIconMenuItem. Since our class is derived from MenuItem, we can use it polymorphically wherever we can use a MenuItem.

Note that, in addition to overriding the appropriate methods (as described below), you also need to set the OwnerDraw property of your class to TRUE. This is an important step, because you won’t see your icon displayed on your menu item if this isn’t done, since the standard Framework code will draw the menu item rather than your overridden methods.

When a menu is pulled down, the Framework first calls OnMeasureItem for each owner-drawn menu item to determine how big the menu needs to be (the sum of the heights and the largest of the widths), then calls OnDrawItem for each owner-drawn menu item to actually do the drawing. That’s why we need to override two methods rather than one.

Note also that these methods are the methods that raise the MeasureItem and DrawItem events. Since other objects may be listening to these events, we need to make sure the events get raised properly by calling the base class’s implementation of our methods.

Now let’s examine the two methods that need to be overridden. OnMeasureItem gets called when the menu needs to be drawn. It allows you to specify the size of your menu by setting some properties of MeasureItemEventArgs, which is passed in as a parameter to this method. The properties you need to set are ItemHeight and ItemWidth. You’ll need the height of the menu item, which is the greater of the icon height or the font height plus a few pixels, and the width of the menu. You can use the StringFormat object (in the System.Drawing namespace) to retrieve the width. And don’t forget: When measuring the width of the menu item, you need to account for the icon also. OnMeasureItem should look something like this:

Protected Overrides Sub OnMeasureItem(ByVal e As MeasureItemEventArgs) MyBase.OnMeasureItem(e) ' MUST be called, best to call first Dim sf As StringFormat = New StringFormat()

 sf.HotkeyPrefix = HotkeyPrefix.Show sf.SetTabStops(50, New Single() {0})

 If Me.icon.Height > Me.font.Height Then e.ItemHeight = Me.icon.Height + 6 Else e.ItemHeight = Me.font.Height + 6 End If e.ItemWidth = CInt(e.Graphics.MeasureString(AppendShortcut(), _ Me.font, 1000, sf).Width) + Me.icon.Width + 5 sf.Dispose() End Sub

With the size of your menu specified, you need to draw it in your class’s OnDrawItem method. This method gets passed a DrawItemEventArgs object. Use this object to obtain a Graphics object for your menu item. This allows you to draw directly on the surface of your menu using the powerful GDI+ features available in the .NET Framework. First, draw a background color, then draw your icon, and finally draw the text of our menu item using the following code:

Protected Overrides Sub OnDrawItem(ByVal e As DrawItemEventArgs) Dim br As Brush Dim sf As StringFormat

 MyBase.OnDrawItem(e) e.Graphics.FillRectangle(SystemBrushes.Control, e.Bounds) If Not (Me.icon Is Nothing) Then e.Graphics.DrawIcon(Me.icon, e.Bounds.Left + 3, e.Bounds.Top + 3) End If

 sf = New StringFormat() sf.HotkeyPrefix = HotkeyPrefix.Show sf.SetTabStops(50, New Single() {0}) br = New SolidBrush(SystemColors.WindowText) e.Graphics.DrawString(AppendShortcut(), Me.font, br, e.Bounds.Left _ + Me.icon.Width + 10, e.Bounds.Top + 2, sf) 'Clean up resources br.Dispose() sf.Dispose() End Sub 

If you have been paying attention (and the good doctor is sure you have), you are probably wondering about the call to AppendShortcut in both OnMeasureItem and OnDrawItem. Why is this call being made? Well, you could just measure and draw your string based on your menu item’s Text property, which would not take into account that your menu item might have a shortcut key. If it does have a shortcut key, you need to display it and increase the width of the menu item. You need a way to do this. AppendShortcut represents the shortcut key as a string, and appends that string to your existing menu item text. Here is the code for AppendShortcut:

Private Function AppendShortcut() As String Dim s As String s = Me.Text ' Check to see if you have a shortcut ' If so, append it to your existing text If Me.ShowShortcut And Me.Shortcut <> Shortcut.None Then Dim k As Keys = CType(Shortcut, Keys) s = s & Convert.ToChar(9) & _ TypeDescriptor.GetConverter(GetType(Keys)).ConvertToString(k) End If Return s End Function 

There you go. The good doctor only had to tap into a small percentage of his energy reserves, so he is rewarding himself with cookies and milk. Now, let’s put our cardigans back on and get ready to go. As soon as the .NET Framework ships (Real Soon Now), we’ll all be able to do this for real, and leave the Land of Make Believe. Good luck, neighbor!

來源:Macromedia

The documentation below describes the scripting interface for the Flash Player ActiveX control. This control handles playback of Flash content on Windows machines which support ActiveX (all 32-bit versions of Windows, beginning with Windows 95.) These methods may also be available to PocketPC Devices using the Flash Player for PocketPC (see the Flash Player for PocketPC FAQ for details.)

Experienced scripters should first read the article Scripting with Flash for an overview of JavaScript methods which can control the Flash Player. Beginning scripters may benefit more from the example based TechNotes “An example of communication between JavaScript and Flash” (TechNote 15683) and “An example of communication between Flash movies” (TechNote 15692).

Note: Most of these methods, properties and events are not available in the Netscape Navigator plug-in version of the Macromedia Flash Player, only the Flash Player ActiveX control for Windows.

Properties

ReadyState (get only) – 0=Loading, 1=Uninitialized, 2=Loaded, 3=Interactive, 4=Complete.

TotalFrames (get only) – Returns the total number of frames in the movie. This is not available until the movie has loaded. Wait for ReadyState = 4.

FrameNum (get or set) – The currently displayed frame of the movie. Setting this will advance or rewind the movie.

Playing (get or set) – True if the movie is currently playing, false if it is paused.

Quality (get or set) – The current rendering quality (0=Low, 1=High, 2=AutoLow, 3=AutoHigh). This is the same as the QUALITY parameter.

ScaleMode (get or set) – Scale mode (0=ShowAll, 1= NoBorder, 2 = ExactFit). This is the same as the SCALE parameter.

AlignMode (get or set) – The align mode consists of bit flags. (Left=+1, Right=+2, Top=+4, Bottom=+8). This is the same as the SALIGN parameter.

BackgroundColor (get or set) – Override the background color of a movie. An integer of the form red*65536+green*256+blue use -1 for the default movie color.

Loop (get or set) – True if the animation loops, false to play once. Same as the MOVIE parameter.

Movie (get or set) – The URL source for the Flash Player movie file. Setting this will load a new movie into the control. Same as the MOVIE parameter.

Methods

Play() – Start playing the animation.

Stop() – Stop playing the animation.

Back() – Go to the previous frame.

Forward() – Go to the next frame.

Rewind() – Go to the first frame.

SetZoomRect(int left, int top, int right, int bottom) – Zoom in on a rectangular area of the movie. Note that the units of the coordinates are in twips (1440 units per inch). To calculate a rectangle in Flash, set the ruler units to Points and multiply the coordinates by 20 to get TWIPS.

Zoom(int percent) – Zoom the view by a relative scale factor. Zoom(50) will double the size of the objects in the view. Zoom(200) will reduce the size of objects in the view by one half.

Pan(int x, int y, int mode) – Pan a zoomed in movie. The mode can be: 0 = pixels, 1 = % of window.

Events

OnProgress(int percent) – Generated as the Flash Player movie is downloading.

OnReadyStateChange(int state) – Generated when the ready state of the control changes. The possible states are 0=Loading, 1=Uninitialized, 2=Loaded, 3=Interactive, 4=Complete.

FSCommand(string command, string args) – This event is generated when a GetURL action is performed in the movie with a URL and the URL starts with “FSCommand:”. The portion of the URL after the : is provided in command and the target is provided in args. This can be used to create a response to a frame or button action in the Shockwave Flash movie.

HTML

The HTML to insert the Flash Player ActiveX control and a movie in a page is:

<OBJECT ID="map" CLASSID="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" CODEBASE="http://active.macromedia.com/flash5/cabs/swflash.cab#version=5,0,0,0" width=300 height=150 align=center> <param NAME="Movie" VALUE="map.swf"> <param NAME="Play" VALUE="False"> <param NAME="Loop" VALUE="False"> <param NAME="Quality" VALUE="High"> <param NAME="Scale" VALUE="ShowAll"> <param NAME="SAlign" VALUE="TR"> <param NAME="BGColor" VALUE="808080"> </OBJECT>

新浪科技:

  多数科学家认为,在地球生命形成之前,作为生命基本组成部分的氨基酸分子,最初是通过地球上的交互作用形成的或者是通过彗星陨石的碰撞而被带来的。加利福尼亚斯克里普斯研究所的分子化学家瑞泽-查德理提出:“这些氨基酸是如何形成生命细胞所必须的缩氨酸的呢?这是一个最主要问题。”

  缩氨酸是氨基酸链子,它们形成活细胞的主要成分蛋白质。科学家至今还没有找到一个合理的化学反应,表明在生命存在之前也就是所谓的前生物期在地球上形成缩氨酸,因此,科学家表示生命不可能通过化学方式进化。

  查德理和他的同事在《科学》杂志上的报告中指出,一种简单的火山气体羰基硫化物(COS),帮助自由漂浮的氨基酸形成缩氨酸。根据查德理的报告,这种可以在各种情况下发生的反应也许在地球生命进化之前就存在地球上了,他指出,金属离子,譬如钢和铅以及其它的化学制品加强了这种反应。

  查德理说:“因为它可以通过各种方式完成,因此它对于前生物条件来说是非常适合的。”

  科罗拉多大学的分子生物学家诺曼-帕斯表示,他对这个发现并不感到惊奇,他说:“各种有机化学都是火山渗出液带来的,海底火山物质是更复杂的,而它们几乎还没被探测过。”

  查德理和他的同事一起从事这个有关生命起源的调查研究,他们的发现可能是一个比较少见的环节。查德理说:“我们只能推测,但是现在可以证明COS是一个有用的在化学上可行的凝聚物。”

  研究者指出,自从COS在一个地质时期比较快的分解之后,它就不可能再在大气中积聚成比较大的浓度。相反,研究人员推测,火山气体可能影响了海底热液出口附近的氨基酸,形成缩氨酸链条,可以化学地粘住附近岩石并继续延长。

  帕斯认为根据报告推断生命起源于火山是不正确的,但是,查德理说:“对于前生物的整体猜想是有立足之处的,因为它可能是确实发生过的事情。”

  新华社电 科学家已经发现了许多在癌症中起作用的因素,但到底是什么因素打响了细胞癌变的第一枪?英国科学家的一项研究,为一个老理论提供了新证据:染色体不稳定、数目异常,可能是癌症的根源。

  据美国《科学》杂志网站12日报道,几乎所有癌细胞的染色体数目都不正常,有的丢失了染色体,有的获得多余的染色体。早在1914年,一位德国生物学家就提出了染色体数目异常导致癌症的理论,但一直缺乏有力证据支持。

  英国癌症研究所的科学家说,他们对8个家庭进行DNA检测,这些家庭里都存在一种遗传病,患者体内超过四分之一的细胞的染色体数目不正常,而且儿童期癌症发病率比常人高得多。

  科学家发现,有5名儿童体内一个称为BUB1B的基因发生了变异。这些儿童体内染色体数目不正常的细胞比例特别高,其中两人已经患有癌症。科学家说,BUB1B基因的功能是帮助保证细胞分裂时传递正确数量的染色体,上述发现显示,染色体数目异常与癌症之间存在直接的因果关系。

  这一成果是寻找癌症起因的重大进展,但有关规律是否适用于所有癌症,还需要进一步研究证实。

來源:39健康網

  登革热近日搅得人心惶惶,登革热是什么?遍布区域在哪里?我们怎么预防?本刊特约军事医学专家撰稿,解析登革热,并介绍我国著名蚊类学家有关蚊虫防治的综合治理理论及方法。

  登革热和登革出血热是由蚊虫传播的病毒性虫媒病,也就是通过蚊虫的刺叮吸血传播的。它的病原是黄病毒科黄病毒属的登革病毒,重要媒介是埃及伊蚊和白纹伊蚊。主要流行区分布在热带和亚热带100多个国家和地区,威胁着全球2亿以上人口的健康,每年向世界卫生组织报告的发病人数有数千万人,有50万的住院病例,其中90%的病人是15岁以下的儿童,平均的死亡率为5%,死亡在2万人以上,它是仅次于疟疾的重要热带病。近二十年来,登革热和登革出血热不仅在东南亚流行趋势加重,而且在世界许多国家和地区复燃。登革出血热由于症状严重,死亡率高而受到人们的关注。

主要流行于热带亚热带

  登革出血热是第二次世界大战出现的新疾病,上世纪50-70年代,登革出血热的分布只局限于少数的国家,80年代以来,迅速扩散。目前登革出血热在东南亚、太平洋岛屿、加勒比海、中南美洲出现,已经成为上述地区重要的公共卫生问题。人们预测,随着全球变化的加剧,包括气候变暖、城市化和国际交通的发展,登革热和登革出血热的地理流行区还将进一步的扩大。

“花蚊子”是媒介

  一般来讲,登革热和登革出血热的传播,主要是人在流行区受到带有登革热病毒的蚊虫的刺叮而感染病毒后,在潜伏期旅行将病毒带到另一个地区,在当地存在着媒介蚊虫和易感人群,通过媒介的吸血将登革热病毒在人群中传播从而引起流行。目前还没有可以使用的疫苗来预防登革热和登革出血热,所以,防治媒介蚊虫是预防和控制流行的重要手段。

  埃及伊蚊是深褐或暗黑而具有银白花斑的“花蚊子”,它广泛地分布在全球热带地区,但也扩展到亚热带地区。根据我国的调查,埃及伊蚊的分布在我国很局限,主要分布在海南省,其次是广东省和广西壮族自治区的部分地区和个别岛屿,1991年广西壮族自治区称当地已消灭了埃及伊蚊。在我国,埃及伊蚊是典型的嗜人血的“家蚊”,雌蚊在白昼吸血,并有间断吸血的习性,在吸血时受到干扰就马上飞离宿主,然后再重新寻找宿主吸血,这样就增加了传播疾病的危险。在国外一些地区,埃及伊蚊广泛地孳生在住区的各类人工和植物容器积水中,而在我国,它不仅只见于居民区,而且主要孳生在室内的饮用水缸积水中,其次是户内外积聚雨水的缸罐中,并且埃及伊蚊是在室内吸血栖息的。

  登革热和登革出血热的另一个重要媒介蚊虫——白纹伊蚊,是有白斑和银白斑的黑色或深褐色蚊虫,俗称“花蚊子”,它在我国分布以北纬34度以南的地区比较常见,在北方有些省份(区)分布较窄。

  白纹伊蚊的原发地是东南亚和亚洲温带地区,后扩散到夏威夷,南达澳大利亚,西至非洲的索马里和马达加斯加,近年来白纹伊蚊也被动地扩散到美国、墨西哥、巴西以及欧洲的一些国家。1985年,白纹伊蚊在美国得克萨斯州被发现,以后扩展到近30个州,由于白纹伊蚊不仅是登革热的重要媒介而且与多种虫媒病毒病有关,引起很大的轰动,称为“亚洲虎蚊”,白纹伊蚊被带到美洲被认为是第二次世界大战以来最严重的医学昆虫事件。

  白纹伊蚊也是白昼活动、凶猛吸血的蚊虫,雌蚊非常活跃而且是喜好人血,只要人走近白纹伊蚊的孳生或栖息地,很快就会引来雌蚊吸血。它们多在室外吸血攻击人,但也会侵入室内吸血。白纹伊蚊主要在人工或植物容器积水中孳生,如花盆、缸罐、轮胎、植物叶腋、竹筒、树洞等等。在我国的南方几乎所有能够集聚雨水的物件都可能孳生白纹伊蚊。特别值得一提的是轮胎积水是白纹伊蚊喜好的孳生场所。

  近20年来,登革热和登革出血热在世界很多国家和地区严重起来,有很多的原因。城市化的发展,人口大量地迁入城镇,导致自来水供应、垃圾处理等市政设施建设不能很快满足人口膨胀的需求,产生了大量的媒介蚊虫孳生地,如饮水储存容器、垃圾废弃物中的瓶瓶罐罐、废旧轮胎等等,因此媒介蚊虫的数量增多。另一方面,由于航空客运的发展,登革热病毒从一个国家或地区传到另一个国家或地区的机会多了起来。除此以外,一些研究和预测指出,全球气候变暖将有可能加重登革热的流行,特别是地理分布区将进一步扩大。

灭蚊方法及其立法

  目前媒介蚊虫的防治需要采用综合治理的理论和方法,我国著名蚊类学家陆宝麟院士提出综合治理的概念和理论是:从蚊虫与生态环境和社会条件的整体出发,根据本标兼治而以治本为主,以及安全(包括对环境无害)、有效、经济和简便的原则,因地因时制宜地对防治的对象采用各种合理手段和有效方法,组成一套系统的防治措施,把防治的蚊虫种群抑制到不足为害的水平,以达到除害灭病的目的。

  登革热和登革出血热的控制,主要采用环境治理、使用杀虫剂进行化学防治的方法相结合控制成蚊和幼虫。通过环境治理,包括翻缸倒罐等方法消除蚊虫幼虫孳生地,消灭孳生地中的幼虫,是预防和控制登革热和登革出血热爆发流行的一个有效手段,需要持之以恒。

  我国爱国卫生运动中的“除四害”达标和创建国家卫生城市的活动,是非常好的预防登革热和登革出血热的形式,通过消除蚊虫孳生地为主要目标的城市灭蚊达标,使媒介蚊虫的密度维持在一个比较低的水平,降低了一个地区登革热和登革出血热爆发的危险性。同时在登革热和登革出血热爆发流行时,使用杀虫剂杀灭室内外成蚊是必需的和紧急的方法。世界卫生组织建议可以使用马拉硫磷、杀螟硫磷等进行超低容量或热雾喷雾,杀灭孳生地周围的成蚊。室内可以采用合适的杀虫剂剂型,如纸烟剂、块烟剂熏杀成蚊或喷洒拟除虫菊酯杀虫剂。需要到登革热和登革出血热流行的国家和地区的人员,可用蚊虫驱避剂涂抹在暴露的皮肤上,喷洒在衣领、袜子等处防止蚊虫的叮咬。

  我国在长期的媒介蚊虫的综合治理中,积累了理论基础和实践经验。一些城市相继颁布了包括灭蚊在内的除四害地方法规。一些东南亚国家,例如新加坡和马来西亚为了预防和控制登革热,已有专门的国家法律,“杀灭病媒昆虫法”,如果居民家中孳生有蚊虫,就会受到处罚,包括罚款和拘留。马来西亚规定:居民和企业有登革热媒介蚊虫的孳生,就是违法,初犯可以罚款1000元马币以内,坐牢三个月以内,或两者兼施。在新加坡出现这种情况可以罚款1000新元或拘留3个月。这对我国也有借鉴意义。

作者:朱春雷  來源:http://blog.csdn.net/zjzcl

一、软件测试概述

    软件测试是软件开发过程的重要组成部分,是用来确认一个程序的品质或性能是否符合开发之前所提出的一些要求。软件测试的目的,第一是确认软件的质量,其一方面是确认软件做了你所期望的事情(Do the right thing),另一方面是确认软件以正确的方式来做了这个事件(Do it right)。第二是提供信息,比如提供给开发人员或程序经理的反馈信息,为风险评估所准备的信息。第三软件测试不仅是在测试软件产品的本身,而且还包括软件开发的过程。如果一个软件产品开发完成之后发现了很多问题,这说明此软件开发过程很可能是有缺陷的。因此软件测试的第三个目的是保证整个软件开发过程是高质量的。

软件质量是由几个方面来衡量的:一、在正确的时间用正确的的方法把一个工作做正确(Doing the right things right at the right time.)。二、符合一些应用标准的要求,比如不同国家的用户不同的操作习惯和要求,项目工程中的可维护性、可测试性等要求。三、质量本身就是软件达到了最开始所设定的要求,而代码的优美或精巧的技巧并不代表软件的高质量(Quality is defined as conformance to requirements, not as “goodness” or “elegance”.)。四、质量也代表着它符合客户的需要(Quality also means “meet customer needs”.)。作为软件测试这个行业,最重要的一件事就是从客户的需求出发,从客户的角度去看产品,客户会怎么去使用这个产品,使用过程中会遇到什么样的问题。只有这些问题都解决了,软件产品的质量才可以说是上去了。

    测试人员在软件开发过程中的任务:

1、寻找Bug

2、避免软件开发过程中的缺陷;

3、衡量软件的品质;

4、关注用户的需求。

总的目标是:确保软件的质量。

 

二、常用的软件测试方法

1. 黑盒测试

    黑盒测试顾名思义就是将被测系统看成一个黑盒,从外界取得输入,然后再输出。整个测试基于需求文档,看是否能满足需求文档中的所有要求。黑盒测试要求测试者在测试时不能使用与被测系统内部结构相关的知识或经验,它适用于对系统的功能进行测试。

黑盒测试的优点有:
1
)比较简单,不需要了解程序内部的代码及实现;

2)与软件的内部实现无关;

3)从用户角度出发,能很容易的知道用户会用到哪些功能,会遇到哪些问题;

4)基于软件开发文档,所以也能知道软件实现了文档中的哪些功能;

5)在做软件自动化测试时较为方便。

黑盒测试的缺点有:
1
)不可能覆盖所有的代码,覆盖率较低,大概只能达到总代码量的30%

2)自动化测试的复用性较低。

    2. 白盒测试

白盒测试是指在测试时能够了解被测对象的结构,可以查阅被测代码内容的测试工作。它需要知道程序内部的设计结构及具体的代码实现,并以此为基础来设计测试用例。如下例程序代码:

 

HRESULT Play( char* pszFileName )

       {

              if ( NULL == pszFileName )

return;

              if ( STATE_OPENED == currentState )

              {

                   PlayTheFile();

              }

              return;

       }

 

读了代码之后可以知道,先要检查一个字符串是否为空,然后再根据播放器当前的状态来执行相应的动作。可以这样设计一些测试用例:比如字符串(文件)为空的话会出现什么情况;如果此时播放器的状态是文件刚打开,会是什么情况;如果文件已经在播放,再调用这个函数会是什么情况。也就是说,根据播放器内部状态的不同,可以设计很多不同的测试用例。这些是在纯粹做黑盒测试时不一定能做到的事情。

    白盒测试的直接好处就是知道所设计的测试用例在代码级上哪些地方被忽略掉,它的优点是帮助软件测试人员增大代码的覆盖率,提高代码的质量,发现代码中隐藏的问题。

白盒测试的缺点有:

1)程序运行会有很多不同的路径,不可能测试所有的运行路径;

2)测试基于代码,只能测试开发人员做的对不对,而不能知道设计的正确与否,可能会漏掉一些功能需求;

3)系统庞大时,测试开销会非常大。

    3. 基于风险的测试

基于风险的测试是指评估测试的优先级,先做高优先级的测试,如果时间或精力不够,低优先级的测试可以暂时先不做。有如下一个图,横轴代表影响,竖轴代表概率,根据一个软件的特点来确定:如果一个功能出了问题,它对整个产品的影响有多大,这个功能出问题的概率有多大?如果出问题的概率很大,出了问题对整个产品的影响也很大,那么在测试时就一定要覆盖到。对于一个用户很少用到的功能,出问题的概率很小,就算出了问题的影响也不是很大,那么如果时间比较紧的话,就可以考虑不测试。

 

基于风险测试的两个决定因素就是:该功能出问题对用户的影响有多大,出问题的概率有多大。其它一些影响因素还有复杂性、可用性、依赖性、可修改性等。测试人员主要根据事情的轻重缓急来决定测试工作的重点。

    4. 基于模型的测试

    模型实际上就是用语言把一个系统的行为描述出来,定义出它可能的各种状态,以及它们之间的转换关系,即状态转换图。模型是系统的抽象。基于模型的测试是利用模型来生成相应的测试用例,然后根据实际结果和原先预想的结果的差异来测试系统,过程如下图所示。

 

三、软件测试的类型

    常见的软件测试类型有:

BVT (Build Verification Test)

BVT是在所有开发工程师都已经检入自己的代码,项目组编译生成当天的版本之后进行,主要目的是验证最新生成的软件版本在功能上是否完整,主要的软件特性是否正确。如无大的问题,就可以进行相应的功能测试。BVT优点是时间短,验证了软件的基本功能。缺点是该种测试的覆盖率很低。因为运行时间短,不可能把所有的情况都测试到。

Scenario Tests(基于用户实际应用场景的测试)

在做BVT、功能测试的时候,可能测试主要集中在某个模块,或比较分离的功能上。当用户来使用这个应用程序的时候,各个模块是作为一个整体来使用的,那么在做测试的时候,就需要模仿用户这样一个真实的使用环境,即用户会有哪些用法,会用这个应用程序做哪些事情,操作会是一个怎样的流程。加了这些测试用例后,再与BVT、功能测试配合,就能使软件整体都能符合用户使用的要求。Scenario Tests优点是关注了用户的需求,缺点是有时候难以真正模仿用户真实的使用情况。

    Smoke Test

在测试中发现问题,找到了一个Bug,然后开发人员会来修复这个Bug。这时想知道这次修复是否真的解决了程序的Bug,或者是否会对其它模块造成影响,就需要针对此问题进行专门测试,这个过程就被称为Smoke Test。在很多情况下,做Smoke Test是开发人员在试图解决一个问题的时候,造成了其它功能模块一系列的连锁反应,原因可能是只集中考虑了一开始的那个问题,而忽略其它的问题,这就可能引起了新的BugSmoke Test优点是节省测试时间,防止build失败。缺点是覆盖率还是比较低。

此外,Application Compatibility Test(兼容性测试),主要目的是为了兼容第三方软件,确保第三方软件能正常运行,用户不受影响。Accessibility Test(软件适用性测试),是确保软件对于某些有残疾的人士也能正常的使用,但优先级比较低。其它的测试还有Functional Test(功能测试)、Security Test(安全性测试)、Stress Test(压力测试)、Performance Test(性能测试)、Regression Test(回归测试)、Setup/Upgrade Test(安装升级测试)等。

 

四、微软的软件测试工作

    1. 基本情况

测试在微软公司是一项非常重要的工作,微软公司在此方面的投入是非常巨大的。微软对测试的重视表现在工程开发队伍的人员构成上,微软的项目经理、软件开发人员和测试人员的比例基本是133144,可以看出开发人员与测试人员的比例是11。对于测试的重视还表现在最后产品要发布的时候,此产品的所有相关部门都必须签字,而测试人员则具有绝对的否决权。

测试人员中分成两种职位,Software Development Engineer in Test(测试组的软件开发工程师)实际上还是属于开发人员,他们具备编写代码的能力和开发工具软件的经验,侧重于开发自动化测试工具和测试脚本,实现测试的自动化。Software Test Engineer(软件测试工程师)具体负责测试软件产品,主要完成一些手工测试以及安装配置测试。

    2. 测试计划

测试计划是测试人员管理测试项目,在软件中寻找Bug的一种有效的工具。测试计划主要有两个作用,一是评判团队的测试覆盖率以及效率,让测试工作很有条理的逐步展开。二是有利于与项目经理、开发人员进行沟通。有了测试计划之后,他们就能够知道你是如何开展测试工作的,他们也会从中提出很多有益的意见,确保测试工作顺利进行。总之,有了测试计划可以更好的完成测试工作,确保用户的满意度。

    测试人员在编写测试计划之前,应获得以下文档:

    1)程序经理编写的产品功能说明书或产品开发计划;

    2)程序经理或开发人员提供的开发进度表。

    根据产品的特性及开发进度安排,测试人员制定具体的测试计划。测试计划通常包括以下内容:

    1)测试目标和发布条件:

    a. 给出清晰的测试目标描述;

    b. 定义产品的发布条件,即在达到何种测试目标的前提下才可以发布产品的某个特定版本。

    2)待测产品范围:

    a. 软件主要特性/功能说明,即待测软件主要特性的列表;

    b. 特性/功能测试一览,应涵盖所有特性、对话框、菜单和错误信息等待测内容,并列举每个测试范围内要重点考虑的关键功能。

    3)测试方法描述:

    a. 定义测试软件产品时使用的测试方法;

    b. 描述每一种特定的测试方法可以覆盖哪些测试范围。

    4)测试进度表:

    a. 定义测试里程碑;

    b. 定义当前里程碑的详细测试进度。

    5)测试资源和相关的程序经理/开发工程师:

    a. 定义参与测试的人员;

    b. 描述每位测试人员的职责范围;

    c. 给出与测试有关的程序经理/开发工程师的相关信息。

    6)配置范围和测试工具:

    a. 给出测试时使用的所有计算机平台列表;

    b. 描述测试覆盖了哪些硬件设备;

    c. 测试时使用的主要测试工具。

    此外,还应列出测试中可能会面临的风险及测试的依赖性,即测试是否依赖于某个产品或某个团队。比如此项测试依赖性WindowsCE这个操作系统,而这个系统要明年2月份才能做好,那么此项测试就可能只有在明年5月份才能完成,这样就存在着依赖关系。如果那个团队的开发计划往后推,则此项测试也会被推迟。

    3. 测试用例开发

    一个好的测试用例就是有一个合理的概率来找到Bug,不要冗余,要有针对性,一个测试只针对一件事情。特别是功能测试的时候,如果一个测试是测了两项功能,那么如果测试结果失败的话,就不知道到底是哪项功能出了问题。

    测试用例开发中主要使用的技术有等价类划分,边界值的分析,Error Guessing Testing

等价类划分是根据输入输出条件,以及自身的一些特性分成两个或更多个子集,来减少所需要测试的用例个数,并且能用很少的测试用例来覆盖很多的情况,减少测试用例的冗余度。在等价类划分中,最基本的划分是一个为合法的类,一个为不合法的类。

    边界值的分析是利用了一个规律,即程序最容易发生错误的地方就是在边界值的附近,它取决于变量的类型,以及变量的取值范围。一般对于有n个变量时,会有6n+1个测试用例,取值分别是min-1, min, min+1, normal, max-1, max,max+1的组合。边界值的分析的缺点,是对逻辑变量和布尔型变量不起作用,还有可能会忽略掉某些输入的组合。

    Error Guessing Testing完全靠的是经验,所设计的测试用例就是常说的猜测。感觉到软件在某个地方可能出错,就去设计相应的测试用例,这主要是靠实际工作中所积累的经验和知识。其优点是速度快,只要想得到,就能很快设计出测试用例。缺点就是没有系统性,无法知道覆盖率会有多少,很可能会遗漏一些测试领域。

    实际上在微软是采用一些专门的软件或工具负责测试用例的管理,有一些测试信息可以被记录下来,比如测试用例的简单描述,在哪些平台执行,是手工测试还是自动测试,运行的频率是每天运行一次,还是每周运行一次。此外还有清晰的测试通过或失败的标准,以及详细记录测试的每个步骤。

    4. Bug跟踪过程

    在软件开发项目中,测试人员的一项最重要使命就是对所有已知Bug进行有效的跟踪和管理,保证产品中出现的所有问题都可以得到有效的解决。一般地,项目组发现、定位、处理和最终解决一个Bug的过程包括Bug报告、Bug评估和分配、Bug处理、Bug关闭等四个阶段:

    1)测试工程师在测试过程中发现新的Bug后,应向项目组报告该Bug的位置、表现、当前状态等信息。项目组在Bug数据库中添加该Bug的记录。

    2)开发经理对已发现的Bug进行集中讨论,根据Bug对软件产品的影响来评估Bug的优先级,制定Bug的修正策略。按照Bug的优先级顺序和开发人员的工作安排,开发经理将所有需要立即处理的Bug分配给相应的开发工程师。

    3)开发工程师根据安排对特定的Bug进行处理,找出代码中的错误原因,修改代码,重新生成产品版本。

    4)开发工程师处理了Bug之后,测试人员需要对处理后的结果进行验证,经过验证确认已正确处理的Bug被标记为关闭(Close)状态。测试工程师既需要验证Bug是否已经被修正,也需要确定开发人员有没有在修改代码的同时引入新的Bug

    5. Bug的不同处理方式

    在某些情况下,Bug已处理并不意味着Bug已经被修正。开发工程师可以推迟Bug的修正时间,也可以在分析之后告知测试工程师这实际上不是一个真正的Bug。也就是说,某特定的Bug经开发工程师处理之后,该Bug可能包括以下几种状态。

    已修正:开发工程师已经修正了相应的程序代码,该Bug不会出现了。

    可推迟:该Bug的重要程度较低,不会影响当前应提交版本的主要功能,可安排在下一版本中再行处理。

    设计问题:该Bug与程序实现无关,其所表现出来的行为完全符合设计要求,对此应提交给程序经理处理。

    无需修正:该Bug的重要程度非常低,根本不会影响程序的功能,项目组没有必要在这些Bug上浪费时间。

 

五、成为优秀测试工程师的要求

    要成为一名优秀的测试工程师,首先对计算机的基本知识要有很好的了解,精通一门或多门的编程语言,具备一定的程序调试技能,掌握测试工具的开发和使用技术。同时要比较细心,会按照任务的轻重缓急来安排自己的工作,要有很好的沟通能力。此外,还要善于用非常规的方式思考问题,尽可能多的参加软件测试项目,在实践中学习技能,积累经验,不断分析和总结软件开发过程中可能出错的环节。这样,一名优秀的测试工程师就从软件测试的实践中脱颖而出了。

 

 

  结束语:微软的软件开发经验积淀深厚,微软工程师们的授课生动溢彩,其中有些内容是结合编程代码所作的详细讲解,较难用介绍性文字加以概括提炼,加之笔者受能力和精力所限,只能撷取部分精华内容整理成文以飨读者,因此难免是挂一漏万,甚至会有失误之处,敬请对本系列文章的关注者谅解及指正。最后对微软老师们的辛勤付出再表由衷谢意!

來源:新浪科技

740)this.width=740″ align=middle border=undefined>

  两位美国科学家理查德·阿克塞尔和琳达·巴克因发现人类嗅觉系统的奥秘而荣获今年的诺贝尔医学奖。以下是瑞典科学院决定向这两位美国科学家颁发医学奖的文告内容节选:

  人类的嗅觉长期以来一直是一个非常神秘的领域。人类认识 和记忆1万种不同气味的基本原理一直不为人所知。今年诺 贝尔医学奖或生理学奖的获奖者解决了这一问题。他们所进行的一系列先驱性的研究向我们清楚地阐释了我们的嗅觉系 统是如何运作的。他们发现了一个大型的基因家族。这一基因家族由1000种不同的基因组成(占我们基因总数的百分之 三),这些基因构成了相当数量的嗅觉受体种类。这些受体 位于嗅觉受体细胞之内,这些细胞在鼻上皮的上端,可以探 测到吸入的气味分子。

  每个嗅觉受体细胞只含有一种嗅觉受体,每个受体可以探测 到数量有限的气味。我们的嗅觉受体细胞因此对一些气味很 敏感。这些细胞直接向特定的微型终端传送神经反应过程。 携带同样受体的受体细胞向同样的肾小球传送他们的神经反 应过程。微型终端再向大脑其它的部分传送信息。数个嗅觉 受体所得到的信息在大脑进行综合,形成一种模式。因此, 我们能够在春天时感觉到丁香的香味,并在其它时候记起这 种香味。

  理查德·阿克塞尔(来自美国纽约)和琳达·巴克(来自美国西雅图)1991年联合发表了他们有关嗅觉的基础论文。他 们在论文中形容了嗅觉受体的一千种基因组成的基因家族。 他们随后独立地展开工作。他们进一步通过分子结构和细胞 组织清楚地向我们阐述了嗅觉系统。

  理查德·阿克塞尔和琳达·巴克得出的结论认为每个嗅觉受 体细胞都只表达某一种特定气味受体基因。这个结论出乎人 们的意料。阿克塞尔和巴克继而确定了大脑的第一个中转站 的组织构成。

  理查德·阿克塞尔和琳达·巴克在研究嗅觉系统中所发现的 一般原则看起来也适用于其它感官系统。信息素是一种可以 影响不同社会行为的分子,尤其是在动物身上。理查德·阿 克塞尔和琳达·巴克都独立地发现了两种其它类型的G蛋白 质连结状受体可以探测到信息素。这两种其它类型的G蛋白 质连结状受体位于鼻上皮的上端。舌味蕾上还有另一种类型 的G蛋白质受体受体,而这与味觉有关。

作者:李富金

  Google是著名的搜索引擎,其强大的搜索功能使许多网民爱不释手。利用Google,我们还可以制作网站的站内搜索引擎。利用这个搜索引擎,不仅可以搜索你网站内的文章,而且可以让每个搜索页面都出现你的网站logo或banner,就象是Google专门为你的网站量身定做的搜索引擎,实在是棒极了。最重要的是,制作这个搜索引擎,无需ASP、CGI支持,调用网站logo也无需使用框架,只要简单地加入一段代码就行了。
  在东方法眼http://www.dffy.com)网站上,你使用一下站内搜索功能就可以看到效果了,这是东方法眼网站所使用的代码:


  下面按顺序说明一下修改代码的方法:代码中的T:black是搜索结果中摘要部分的字体颜色,可以修改为其他的颜色,如red,blue,或者干脆是任意颜色代码如#336699。LW:468是搜索结果页面中logo图片的宽度,LH:60则是高度,468和60分别改成你的logo的宽度和高度。http://www.dffy.com/pic/dffyban.gif是logo图片的地址,修改成你要调用图片的地址。AH:center表示搜索结果页面中google和你的logo的位置,center表示居中,可以修改为靠左left或靠右right。VLC:#551A8B是已经打开过的链接颜色,可以不改。S:http://www.dffy.com表示logo链接到的网址,可以修改为你想要指向的网页,一般直接写上你的网址就行了。下面出现的几个www.dffy.com,都换成你的网址,要不可就是搜索我的网站了,一定不能搞错。
  代码修改好以后,直接加入到你的网页的body部分就可以了。试验一下,是不是很酷?

单击“开始→运行”,输入“Regedit”后回车,打开“注册表编辑器”,依次展开 HKEY_CLASSES_ROOT\CLSID\{FB7199AB-79BF-11d2-8D94-0000F875C541}\LocalServer32分支,删除默认键值即可。同样在注册表中,依次展开HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Outlook Express”分支,在窗口右侧新建一个名为“HideMessenger”的DWORD值,然后将其键值设为“2”即可。

当然,最彻底的方法莫过于将Windows Messenger卸载:单击“开始→运行”,输入“Rundll32 advpack.dll,LaunchINFSection windir\INF\msmsgs.inf,BLC.Remove”后回车即可,其中“windir”为Windows XP 的安装目录,如“C:\Windows”。