精华内容
下载资源
问答
  • 将关系数据映射到业务实体

    千次阅读 2012-05-30 14:28:14
    · 带CRUD行为的业务实体组件。按上述方法定义一个自定义实体类,并实现调用与此业务实体相关联的基础数据访问逻辑组件的CRUD方法。 注意: 如果希望以一种更加面向对象的方式使用数据,可以使用另一种替代...

    .net数据设计与应用

    作者:Angela Crocker、Andy Olsen和Edward Jezierski 

    适用于:
    Microsoft® .NET 应用程序
    摘要:
    学习向 Microsoft .NET 应用程序公开数据的最佳方式,以及如何实现一个有效的策略以便在分布式应用程序的层间传递数据。(本文包含一些指向英文站点的链接。)
    目录

    · 简介

    · 将关系数据映射到业务实体

    · 实现数据访问逻辑组件

    · 实现业务实体

    · 事务处理

    · 验证

    · 异常管理

    · 授权与安全性

    · 部署

    · 附录及其合作者

    简介

    在设计分布式应用程序时需要确定如何访问和表示与该应用程序相关联的业务数据。本文提供一些指导原则以帮助您选择公开数据、保持数据和在应用程序的层间传递数据的最佳方式。
    图1 所示为分布式应用程序中的常见层。本文区分业务数据与使用这些数据的业务过程,并且仅在需要明确说明时讨论业务过程层。同样,本文仅在直接涉及数据表示方式(例如 Microsoft® ASP.NET Web 页面公开业务数据的方式)时讨论表示层。图1 中使用了两个新术语:数据访问逻辑组件和业务实体组件。本文后面将解释这些术语。

    clip_image001
    图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 所示为影响数据访问逻辑组件和业务实体实现策略的一些技术因素。本文将分别讨论这些技术因素并提供相关建议。

    clip_image002
    图2:影响数据访问逻辑组件和业务实体设计的技术因素

    将关系数据映射到业务实体

    数据库通常包含许多表,这些表之间的关系通过主键和外键来实现。当定义业务实体以在 .NET 应用程序中表示这些数据时,必须确定如何把这些表映射到业务实体。
    请考虑图 3 所示的假想零售商数据库。

    clip_image003
    图3:假想的关系数据库中的表关系

    下表总结了示例数据库中的关系类型。

    clip_image004

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

    · 获取(或更新)客户的有关信息(包括地址)

    · 获取客户的订单列表

    · 获取特定订单的订购项目列表

    · 创建新订单

    · 获取(或更新)一个或一组产品的有关信息

    为满足这些应用程序要求,该应用程序要处理三个逻辑业务实体:Customer、Order 和 Product。对于每个业务实体,都将定义一个单独的数据访问逻辑组件,如下所示:

    · Customer 数据访问逻辑组件。此类将为检索和修改 Customer 表和 Address 表中的数据提供服务。

    · Order 数据访问逻辑组件。此类将为检索和修改 Order 表和 OrderDetails 表中的数据提供服务。

    · Product 数据访问逻辑组件。此类将为检索和修改 Product 表中的数据提供服务。

    图4 所示为这些数据访问逻辑组件与它们所表示的数据库中的表之间的关系。

    clip_image005
    图4:定义向 .NET 应用程序公开关系数据的数据访问逻辑组件

    将关系数据映射到业务实体的建议
    要将关系数据映射到业务实体,请考虑以下建议:

    · 花些时间来分析您的应用程序的逻辑业务实体并为之建立模型,不要为每个表定义一个单独的业务实体。建立应用程序的工作方式模型的方法之一是使用统一建模语言 (UML)。UML 是一种形式设计注释,用于在面向对象的应用程序中建立对象模型,并获取有关对象如何表示自动过程、人机交互以及关联的信息。有关详细信息,请参阅 Modeling Your Application and Data

    · 不要定义单独的业务实体来表示数据库中的多对多表,可以通过在数据访问逻辑组件中实现的方法来公开这些关系。例如,前面示例中的 OrderDetails 表没有映射到单独的业务实体,而是通过在 Order 数据访问逻辑组件中封装 OrderDetails 表来实现 Order 与 Product 表之间的多对多关系。

    · 如果具有返回特定业务实体类型的方法,请把这些方法放在该类型对应的数据访问逻辑组件中。例如,当检索一个客户的全部订单时,返回值为 Order 类型,因此应在 Order 数据访问逻辑组件中实现该功能。反之,当检索订购某特定产品的全部客户时,应在 Customer 数据访问逻辑组件中实现该功能。

    · 数据访问逻辑组件通常访问来自单一数据源的数据。当需要聚合多个数据源的数据时,建议分别为访问每个数据源定义一个数据访问逻辑组件,这些组件可以由一个能够执行聚合任务的更高级业务过程组件来调用。建议采用这种方法的原因有二:

    o 事务管理集中在业务过程组件中,不需要由数据访问逻辑组件显式控制。如果通过一个数据访问逻辑组件访问多个数据源,则需要把该数据访问逻辑组件作为事务处理的根,这会给仅读取数据的功能带来额外的系统开销。

    o 通常,并不是应用程序的所有区域都需要聚合,并且通过分离对数据的访问,您可以单独使用该类型,也可以在必要时将其用作聚合的一部分。

    实现数据访问逻辑组件

    数据访问逻辑组件是一个无状态类,也就是说,所交换的所有消息都可以独立解释。调用之间不存在状态。数据访问逻辑组件为访问单一数据库(某些情况下可以是多个数据库,例如水平数据库分区)中的一个或多个相关表提供方法。通常,数据访问逻辑组件中的这些方法将调用存储过程以执行相应操作。
    数据访问逻辑组件的主要目标之一是从调用应用程序中隐藏数据库的调用及格式特性。数据访问逻辑组件为这些应用程序提供封装的数据访问服务。具体地说,数据访问逻辑组件处理以下实现细节:

    · 管理和封装锁定模式

    · 正确处理安全性和授权问题

    · 正确处理事务处理问题

    · 执行数据分页

    · 必要时执行数据相关路由

    · 为非事务性数据的查询实现缓存策略(如果适用)

    · 执行数据流处理和数据序列化

    本节后面将详细讨论其中的某些问题。
    数据访问逻辑组件的应用方案
    图5 所示为从各种应用程序类型(包括 Windows 窗体应用程序、ASP.NET 应用程序、XML Web services 和业务过程)中调用数据访问逻辑组件的方式。根据应用程序的部署方式,这些调用可以是本地的,也可以是远程的。

    clip_image006

    图5:数据访问逻辑组件的应用方案(单击缩略图以查看大图像)

    实现数据访问逻辑组件类
    数据访问逻辑组件使用 ADO.NET 执行 SQL 语句或调用存储过程。有关数据访问逻辑组件类的示例,请参阅附录中的如何定义数据访问逻辑组件类。
    如果您的应用程序包含多个数据访问逻辑组件,可以使用数据访问助手组件来简化数据访问逻辑组件类的实现。该组件可以帮助管理数据库连接、执行 SQL 命令以及缓存参数。数据访问逻辑组件仍然封装访问特定业务数据所需的逻辑,而数据访问助手组件则专注于数据访问 API 的开发和数据连接配置,从而帮助减少代码的重复。Microsoft提供了 Data Access Application Block for .NET,当使用 Microsoft SQL Server™ 数据库时,可在您的应用程序中将其用作一个通用的数据访问助手组件。图 6 所示为使用数据访问助手组件帮助实现数据访问逻辑组件的方法。

    clip_image007

    图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 格式检索您的业务实体:

    o 如果您使用的是 SQL Server 2000,则可以在查询或存储过程中使用 FOR XML 子句。在性能测试中,使用 FOR XML 只比返回 DataSet 稍微快一点。

    o 检索 DataSet 并将其转换为 XML 流或以 XML 流的格式写出。这种方法会带来创建 DataSet 的系统开销和额外的转换开销(如果执行转换)。

    o 使用输出参数或数据读取器构建一个 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 数据,而应使用以下技术之一:

    o 按预先排好的顺序提供数据。这种方法不支持在调用应用程序中动态重新排序数据。

    o 应用 XSLT 样式表动态排序数据。如果需要,可以使用 DOM 在运行时改变 XSLT 样式表中的排序条件。

    o 将 XML 数据转换为 DataSet,并使用 DataView 对象排序和搜索数据元素。

    · 使用专用字段。您不能选择隐藏信息。

    将业务实体表示为通用 DataSet
    通用 DataSet 是 DataSet 类的实例,它是在 ADO.NET 的 System.Data 命名空间中定义的。DataSet 对象包含一个或多个 DataTable 对象,用以表示数据访问逻辑组件从数据库检索到的信息。
    图7 所示为用于 Product 业务实体的通用 DataSet 对象。该 DataSet 对象具有一个 DataTable,用于保存产品信息。该 DataTable 具有一个 UniqueConstraint 对象,用于将 ProductID 列标记为主键。DataTable 和 UniqueConstraint 对象是在数据访问逻辑组件中创建该 DataSet 时创建的。

    clip_image008
    图7:用于 Product 业务实体的通用 DataSet

    图8 所示为用于 Order 业务实体的通用 DataSet 对象。此 DataSet 对象具有两个 DataTable 对象,分别保存订单信息和订单详细信息。每个 DataTable 具有一个对应的 UniqueConstraint 对象,用于标识表中的主键。此外,该 DataSet 还有一个 Relation 对象,用于将订单详细信息与订单相关联。

    clip_image009
    图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)将业务实体数据传递到其他层。

    clip_image010

    图9:自定义业务实体组件的作用

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

    · 选择使用结构还是使用类。对于不包含分层数据或集合的简单业务实体,可以考虑定义一个结构来表示业务实体。对于复杂的业务实体或要求继承的业务实体,可将实体定义为类。

    · 表示业务实体的状态。对于数字、字符串等简单值,可以使用等价的 .NET 数据类型来定义字段。有关说明如何定义自定义实体的代码示例,请参阅附录中的如何定义业务实体组件。

    · 表示自定义业务实体组件中的子集合和层次结构。表示自定义实体中的数据的子集合和层次结构的方法有两种:

    o .NET 集合(例如 ArrayList)。.NET 集合类为大小可调的集合提供了一个方便的编程模型,还为将数据绑定到用户界面控件提供了内置的支持。

    o DataSet。DataSet 适合于存储来自关系数据库或 XML 文件的数据的集合和层次结构。此外,如果需要过滤、排序或绑定子集合,也应首选 DataSet。

    o 有关说明如何表示自定义实体中数据的集合和层次结构的代码示例,请参阅附录中的如何表示自定义实体中数据的集合和层次结构。

    · 支持用户界面客户端的数据绑定。如果自定义实体将要由用户界面使用并且希望利用自动数据绑定,可能需要在自定义实体中实现数据绑定。请考虑以下方案:

    o Windows 窗体中的数据绑定。您可以将实体实例的数据绑定到控件而不必在自定义实体中实现数据绑定接口。也可以绑定实体的数组或 .NET 集合。

    o Web 窗体中的数据绑定。如果不实现 IBindingList 接口,则不能将实体实例的数据绑定到 Web 窗体中的控件。然而,如果只想绑定集合,则可以使用数组或 .NET 集合而不必在自定义实体中实现 IBindingList 接口。

    有关说明如何将自定义实体绑定到用户界面控件的代码示例,请参阅附录中的如何将业务实体组件绑定到用户界面控件。

    · 公开内部数据更改的事件。公开事件可以获得丰富的客户端用户界面设计,因为它使得无论数据显示在哪里都可以对其进行刷新。事件应当只针对内部状态,而不是针对服务器上的数据更改。有关说明如何公开自定义实体类中的事件的代码示例,请参阅附录中的如何公开业务实体组件中的事件。

    · 使业务实体可序列化。使业务实体可序列化可以将业务实体的状态保持在中间状态而不进行数据库交互。这样可以方便脱机应用程序的开发和复杂用户界面过程的设计,即在完成前不会影响业务数据。序列化有两种类型:

    o 使用 XmlSerializer 类进行 XML 序列化。如果只需要把公共字段和公共读/写属性序列化为 XML,则可以使用 XML 序列化。注意,如果从 Web 服务返回业务实体数据,对象将通过 XML 序列化自动序列化为 XML。
    您可以对业务实体执行 XML 序列化而无需在实体中实现任何附加代码。然而,只有对象中的公共字段和公共读/写属性被序列化为 XML。专用字段、索引生成器、专用属性、只读属性及对象图不会被序列化。您可以使用自定义实体中的属性控制结果 XML。有关将自定义实体组件序列化为 XML 格式的详细信息,请参阅附录中的如何将业务实体组件序列化为 XML 格式。

    o 使用 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 行为的自定义实体类的作用。
    clip_image011

    图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+ 事务处理的系统开销,建议将事务处理放到数据库中并在存储过程中控制事务性行为(如果可能)。

    clip_image012

    图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 文档时由业务过程对其进行验证。

    · 连续验证。这是在应用程序的许多不同层次上持续进行的一种验证。连续验证的示例包括:

    o 用户界面可以指定字段的最大长度以防止用户输入过长的字符串。

    o DataSet 可以指定数据列的最大长度。

    o 自定义业务实体组件可以对实体数据执行范围检查、长度检查、非空检查以及其他简单测试。

    o 数据访问逻辑组件、存储过程和数据库本身可以执行类似的测试,以便在将数据保存到数据库之前确保其有效性。

    有时,您可能希望实现额外的聚合过程或转换过程。这种方法在验证和转换经常变化时可能很有用,但会损失性能。例如,如果一个 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 类中继承自定义技术异常和自定义业务异常。

    授权与安全性
    学习向 Microsoft .NET 应用程序公开数据的最佳方式,以及如何实现一个有效的策略以便在分布式应用程序的层间传递数据。(本文包含一些指向英文站点的链接。)
    本节说明如何将安全性应用于数据访问逻辑组件和业务实体组件。.NET 公共语言运行库使用权限对象实现其对托管代码的强制限制机制。权限对象有三种,各自具有特定的用途:

    · 代码访问安全性。这些权限对象用于防止未经授权使用资源和操作。

    · 身份标识。这些权限对象指定运行程序集时所必需的身份标识特征。

    · 基于角色的安全性。这些权限对象提供了一个机制,用于判断用户(或用户的代理人)是否具有特定身份标识,或者是否是指定角色的成员。PrincipalPermission 对象是唯一基于角色的安全性权限对象。
    托管代码可以使用 Principal 对象(包含对 Identity 对象的引用)来判断当事人的身份标识或角色。把 Identity 对象和 Principal 对象与用户、组帐户等大家所熟悉的概念比较可能会更容易理解。在 .NET Framework 中,Identity 对象表示用户,而角色表示成员身份和安全性环境。Principal 对象封装了 Identity 对象和角色。.NET Framework 中的应用程序根据 Principal 对象的身份标识或角色成员身份(后者更常见)授予 Principal 对象权限。
    数据访问逻辑组件中的安全性建议
    数据访问逻辑组件的设计目的是供其他应用程序组件使用,它也是您的应用程序代码中在调用程序可以访问数据之前实现安全性的最后一个地方。
    通常,数据访问逻辑组件可以依赖于由调用程序设置的安全性环境。然而,有些情况下数据访问逻辑组件必须执行自己的授权检查,以确定是否允许当事人执行所请求的操作。授权在身份验证后进行,并使用当事人身份标识与角色等有关信息来确定该当事人可以访问的资源。
    在以下情况下,应在数据访问逻辑组件层次上执行授权检查:

    · 需要与不完全信任的业务过程开发人员共享数据访问逻辑组件

    · 需要保护对数据存储提供的强大功能的访问
    在定义了 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 身份验证和连接池的详细信息,
    安全通信建议
    要实现调用应用程序与数据访问逻辑组件之间的安全通信,请考虑以下建议:

    · 如果数据访问逻辑组件是通过各种层的线路调用的,并且信息交换包含需要保护的机密信息,则应使用分布式组件对象模型 (DCOM)、安全套接字层 (SSL)、安全 Internet 协议 (IPSec) 等安全通信技术。

    · 如果数据是加密存储在数据库中,则通常由数据访问逻辑组件负责数据的加密与解密。如果信息暴露会导致巨大损害,则必须考虑保护与数据访问逻辑组件进行通信的通道。
    业务实体组件中的安全性建议
    如果将业务实体实现为数据结构(如 XML 或 DataSet),则不需要实现安全性检查。然而,如果将业务实体实现为带有 CRUD 操作的自定义业务实体组件,请考虑以下建议:

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

    · 业务实体组件不应处理通信安全性或数据加密,应把这些任务留给相应的数据访问逻辑组件。

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

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

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

    · 事务处理可以在业务过程对象和数据访问逻辑组件之间无缝流动。然而,事务处理不能跨越远程通道无缝流动。在远程方案下,需要使用 DCOM 来实现事务处理。此外,如果业务过程与数据访问逻辑组件被防火墙分开,还需要打开这两个物理层之间的防火墙端口以启用 DTC 通信。

    · 一起部署业务过程对象和数据访问逻辑组件可以减少事务处理失败节点的数目。

    · 安全性环境自动在业务过程对象和数据访问逻辑组件之间流动,无需设置当事人对象。

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

    o 不与 UI 代码一起部署数据访问逻辑组件的一个常见原因是防止通过 Web 领域直接对数据源进行网络访问。

    o 如果您的 Web 领域部署在 DMZ 环境中,则必须打开防火墙端口才能访问 SQL Server。如果使用 COM+ 事务处理,还必须为 DTC 通信打开其他的防火墙端口。

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

    · 部署作为有类型的 DataSet 实现的业务实体。有类型的 DataSet 类必须由数据访问逻辑组件和调用应用程序访问。因此,建议在一个要部署在多个层的公共程序集中定义有类型的 DataSet 类。

    · 部署作为自定义业务实体组件实现的业务实体。根据数据访问逻辑组件中定义的方法签名,自定义实体类可能需要由数据访问逻辑组件访问。请遵循与有类型的 DataSet 相同的建议,即在一个要部署在多个层的公共程序集中定义自定义实体类。

    · 部署作为通用 DataSet 或 XML 字符串实现的业务实体。通用 DataSet 和 XML 字符串不表示单独的数据类型。以这两种格式实现的业务实体不存在部署问题。

    附录
    如何定义数据访问逻辑组件类
    以下代码示例定义一个名为 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

    如何定义业务实体组件
    以下示例显示了如何为 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 的属性,以便使调用应用程序能够访问订单详细信息。

    · 该类包含一个附加方法和一个附加属性,用于简化对订单详细信息的访问:

    o IsProductOrdered 方法接收一个 ProductID 参数,并返回一个布尔值以表明该产品是否出现在订单中。

    o 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>

    如何将业务实体组件序列化为 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));
        // 必要时使用更多代码...
      }
      
      // 其他成员,同前...
    }

    如何将业务实体组件序列化为二进制格式
    以下代码示例显示了如何使用 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 序列化的代码相同。
    合作者
    衷心感谢以下投稿人和审阅人:Luca Bolognese、Mike Pizzo、Keith Short、Martin Petersen - Frey (PSS)、Pablo De Grande、Bernard Chen (Sapient)、Dimitris Georgakopoulos (Sapient)、Kenny Jones、Chris Brooks、Lance Hendrix、Pradyumna Siddhartha、Franco A. Ceruti (VBNext)、Diego Gonzalez (Lagash)、Chris Schoon。
    同时,感谢以下内容小组成员:Chris Sfanos、Angela Crocker、Andy Olsen、Sharon Smith。

    原文地址:http://www.cnblogs.com/zhixx/archive/2012/02/21/2360833.html

    展开全文
  • 先说说业务规则。笔者习惯将业务规则分为三种。  一种是全局规则,这种规则一般与所有用例都相关而不是与特定用例相关,例如actor要操作用例必须获得相应的...时候,这类规则也被写到软件架构文档中。关于用例补充

    先说说业务规则。笔者习惯将业务规则分为三种。
        一种是全局规则,这种规则一般与所有用例都相关而不是与特定用例相关,例如actor要操作用例必须获得相应的授权,用例的操作与授权级别相关,或者用户在系统中的所有操作都要被记录下来等等。这类规则笔者习惯于,并且也建议将它们写到用例的补充规约里面去,因为它们一般与具体的业务功能性要求没有直接关系。有时候,这类规则也被写到软件架构文档中。关于用例补充规约以后再讨论。
        第二种是交互规则。这种规则产生于用例场景当中,例如当提交一份定单时,哪些数据是必须填写的,用户身份是否合法。当然也包括一般理解上的业务流程流转规则等,例如金额大于一万元的定单被定为VIP定单进入特殊流程等。这类规则一般要写到用例规约中。交互规则实际上还有两个是比较特殊的,一个入口条件,也称为前置条件,即actor满足什么条件才能启动用例;另一个是出口条件,也称为后置条件,即用例结束后会产生哪些后果。稍后参看示例。
        第三种是内禀规则。所谓内禀规则是指业务实体本身具备的规则,并且不因为外部的交互而变化的规则。例如,每张定单至少要有一件商品,同一类商品数量不能大于5件等。同时也包括大家所熟悉的数据效验规则,例如身份证号必须是15或18位,邮编必须是6位等等。这类规则是业务实体的内在规则,因此应该写到领域模型文档中,稍后参看示例。
        读者或许对把规则这么分类有不同的习惯和理解,可能会觉得麻烦。但是笔者这么做有充分的理由。首先,全局规则与具体用例无关,它实际是系统应该具备的特性,这些规则把它分出来目的就是为了让系统去负责这些特性的实现。读者可以结合实际考虑一下,通常授权,事务,备份等特性都是由系统框架去实现的,具体的功能中并不去实现它们。如果读者有过基于某个框架开发应用的经历的话一定会认同笔者的话。其次,交互规则是在用例场景当中产生的,它们规定了满足什么条件后业务将如何反应。通常,这部分规则是最复杂,也最不稳定,最容易变化的。大家所说的需求经常变更相信绝大部分就来自于此。因此将这些规则单独列出来并给予关注和管理是很有必要的。同时这部分规则通常在系统中以Control类去实现,MVC模式如此,SOA架构中的BPEL也是如此。如果需求无可避免的要变更,那么,将交互规则单独提取出来,并设计成为扩展性很强的结构就是一种有效的应对手段。第三,内禀规则与外部交互无关,不论谁,在什么情况下提交定单,必须有至少有一件商品;不论你在哪个国家,在什么公路上开车,刹车都是必不可少的。这种内在的性质让你想到了什么?面向对象的封装原则对吗?这类规则应该实现到你的实体类中去,不论你的业务实体是EntityBean,JavaBean,POJO还是COM+,根据面向对象的封装原则,内禀的逻辑一定不要让它暴露到外部去。

    这么分类以后,对软件成熟度比较高的组织来说,提供了很好的Level Reference。如果你是架构师,应该关注的是全局规则;如果你是设计师,应该关注的是交互规则;如果你是程序员,应该关注的是内禀规则。
        在这里笔者想说点题外话:)。笔者曾经看过一篇《架构师已死》的文章(很抱歉已经记不起出处和作者,如有冒犯之处请海涵),作者的观点认为架构设计完成后,通常到最后改得面目全非,既然一开始不可能考虑到所有可能,何不如在开发过程中持续总结,重构,最后架构会自然而然出来的,如果这样,架构师有何用处呢?笔者认为这个说法是有一定道理的,不过要看软件组织架构和软件过程的定义,不可一概而论。《架》一文的作者是XP的fans,对XP软件过程来说,本来就不需要架构师这个角色,甚至连设计都不需要,开发,测试,改进,重构...,当然,从一开始就没打算按照一个stable的方法去做,架构师也就没有存在的必要。架构师在XP中已死,不过在RUP中还活着:)。软件界一直对XP和RUP有着争论,笔者无意在本文中界入这个争执,只是话已经到此,就顺便表达一下笔者观点。XP和RUP都是非常优秀的软件方法,只是它们各有各的适应范围。对于中小型公司和中小型软件来说,XP是非常有效的管理方法,它能大大降低管理、开发成本和技术风险。不过要是对于大公司和大型项目来说,XP就不适用了,这时RUP却非常适合。你能想象洛克希德·马丁公司用XP的方法来开发F-35战斗机会是一个什么情形吗?没有人清楚的知道将来的飞机整体是什么样,反正先造一架出来,摔了找找原因,改进改进,重构一下,再造一架....再摔了,没关系,咱们拥抱变更,再造就是了。呵呵,那XP什么情况下适用呢?如果你是一个杂货店的老板,不知道什么样的商品受欢迎,没关系,先各进一小批货,卖上一段时间,受欢迎的货品多进,不受欢迎的不进,跟顾客多交流,顾客喜欢什么商品咱就进什么,不断改进,最后一定会顾客盈门的。这时如果这个老板先做商业分析,客户关系分析,消费曲线分析....还没开业呢,估计就破产了。另外,RUP和XP也不是非此即彼的关系,在造F-35的过程中,对整体飞机来说,用RUP是适合的,具体到发动机的提供商,倒是大可XP一把,最终只要提供合格的发动机就OK了。
        题外话说了不少,书归正传。业务规则分类以后,就应该按分类去调研了。
        全局规则很难从用户处调研得来,通常这方面是用户的盲点。这主要是由有经验的系统分析员,或架构师以及客户方的IT部门(如果有的话),从业务特点、应用环境、行业规定、法律规章等等方面去总结,再求得客户方的认可。
        交互规则从用例场景而来,每一个场景,场景中每一个交互的过程可能都隐含着规则。这就需要与客户多讨论。请参考本系列文章的第3篇关于涉众的讨论,交互规则最主要的来源是业务提出者和业务管理者,最好不要去找业务执行者。
        内禀规则是针对业务实体的,因此要对每个业务实体的属性进行罗列,并找出它们的规则。内禀规则最主要的来源是业务执行者,需求人员应该更多的去与他们交流。

    现在来谈谈业务实体的属性。业务实体的属性在这个阶段应该用业务术语来描述,而非计算机术语。调研范围是上一篇模型中得到的领域模型中的每一个实体,而属性的原始来源是客户的各类实际表单,以及在交流过程中客户提出的各种要求。如果业务实体有状态,并且是比较复杂的,那么在建模的时候应该有一个状态图来说明。请参看本系统文章提供的建模实例中'图书'业务实体下面的状态图。请读者注意,在本文后面提供的例子中,业务实体描述看上去象是一张数据表,但它绝对不是数据表。它是对业务中实体属性的业务角度描述。系统分析不是做设计,脑子里不要有任何关于设计或实现方法的想法,这些想法会误导你做出不适合的决定。你的职责是通过一个合理的模型完整的将业务描述出来,并求得客户方的认可。任何替客户和架构师,设计师甚至程序员“着想”的方法其实都是不恰当的。
        最后本文提供一个用例规约的例子,每个用例都应该写一份用例规约。本文提供的这个例子基本上来自RUP提供的模板,不过在实际工作中笔者做了些修改,供读者参考。这个例子的蓝本还是本系统文章一直在使用的网上图书馆的实例。点这里下载用例规约例子,点这里下载建模实例。
        到这里,需求过程差不多也该结束了。但是我们的确还有一些工作要做。如果读者所工作的组织是用RUP来做需求的,而客户方或者监理方未必会对这种需求文档表示满意。他们会以国标来要求你。同时,到目前为止,我们产生的成果都是一些分离的图形和文档,对于客户来说,这的确是不好的文档结构。难道客户的采购清单里还需要包括Rational Rose,这样才能阅读你提供的需求文档吗?显然这是不可能的,所以,给用户提供的文档还是以传统的《需求规格说明书》为好。下一篇就讨论一下如何将我们的分析成本集成到《需求规格说明书》中。顺带讨论一下用例补充规约和系统原型的产生以及它的作用。敬请期待。

    展开全文
  • 目录写在前一、 强实体与弱实体的定义1. 强实体2. 弱实体百度百科中的解释《数据库系统课程》中的解释总结起来 写在前 数据库设计是困难的,其原因之一就在于我们很难去完全把握实体的定义。是不是实体、该不该...

    写在最前

    数据库设计是困难的,其原因之一就在于我们很难去完全把握实体的定义。是不是实体、该不该定义实体是一直困扰数据库初学者的问题,强实体、弱实体的概念同样难以理解。
    我一直深受强实体、弱实体概念的困扰,百度百科中的定义不能很好地解决我的困惑,一路学习过来,自己对强实体、弱实体的理解越来越深入,因此写下这篇文章与大家分享自己对强实体与弱实体的一些体会。如果觉得有帮助,请点赞鼓励!

    一、 强实体与弱实体的定义

    1. 强实体

    其实例的存在不依赖于任何其他实体类型的实例;有自己独立的主键,唯一性地标识它的每个实例。

    2. 弱实体

    百度百科中的解释

    一个实体对于另一个实体(一般为强实体,也可以是依赖于其他强实体的弱实体)具有很强的依赖联系,而且该实体主键的一部分或全部从其强实体(或者对应的弱实体依赖的强实体)中获得,则称该实体为弱实体。

    《数据库系统课程》中的解释

    其实例的存在依赖于其它实体类型的实例;其主键包括它所依赖的实体类型的主键。

    总结起来

    百度百科中的解释和课程中的解释都是在强调两点:
    第一点:依赖,弱实体应该依赖于强实体;
    第二点:主键,弱实体的主键应该是组合主键(其他实体的主键组成的)。

    二、 关于定义的几个疑惑

    但定义中有几个地方令我不解,我相信初学者多少也会遇到同样的问题。我总结了四点:

    什么叫“依赖”?

    以教务系统数据库为例,如果没有学校,那么学院不再是学院,学生不再是学生,课程更将不复存在,所以这些实体都依赖于其他实体,因此这些都是弱实体?但我们知道,学院、学生一般都作为强实体。因此,定义中的“依赖”指的是什么呢?

    先有鸡还是先有蛋?

    是因为一个实体的主键包括其他实体的主键而使该实体成为了弱实体,还是因为一个实体是弱实体,所以它的主键必须包括其他实体的主键?
    这是一个先鸡(弱实体)还是先蛋(组合主键)的问题。

    为什么要定义弱实体?

    我们知道,弱实体对于另一个实体具有很强的依赖联系,似乎并不是“真实”存在的事物,那么为什么我们还会有弱实体的概念,而不是直接认为实体就是强实体呢?

    什么时候需要定义弱实体?

    有时候弱实体就是需求中的实体,只是它依赖于其他实体,有时候关系也可以认为是弱实体,有时候出于设计的需要我们也会定义弱实体,那么何时需要定义弱实体?

    总结起来,以上四个问题其实是一个问题:

    怎样正确地理解强实体与弱实体的含义?

    三、 唯有实践,方出真知

    有很多事情是很难想明白的,但经过几次数据库设计实战,我发现自己或多或少地定义了一些弱实体。我选取了三个典型的例子:

    教务系统数据库设计(一)

    之后我会专门写一篇博客介绍教务系统数据库的设计过程,这里选取其中一个比较典型的部分。业务需求是这样的:

    一周有七天,每一天有11节。

    上面这句话中涉及到了几个实体?很简单吧,三个实体:周、天、节。那么它们是强实体还是弱实体呢?不好说,但是我们可以确定,概念数据模型的设计应该是这个样子:
    在这里插入图片描述
    需要勾选“Dependent”。首先回答一个问题:为什么不能是下面这个样子?
    在这里插入图片描述
    Day的主键应该是dayOfWeek,如果不用“Dependent”将Day的主键改为weekNum+dayOfWeek,我们的Day表格中只能有七行记录,也就是说对于某一个星期一,我们无法区分这是第几周的星期一!同样的道理,如果Section的主键仅仅为sectionNum,那么我们根本无法区分这一节课是哪一天的课!教务系统要有排课、选课功能,只知道第1节,但不知道是哪一天的第一节,这肯定是不可以的。

    因此,当我们想要找到某一节时,需要同时指定那一周、星期几、哪一节。比方说我可以把《数据库设计》这门课安排在第一周星期一三四节。

    在这个例子中,三个实体都是需求中明明白白告诉我们的实体,但我们将Day与Section都作为了弱实体,因为强实体的Day与Section根本无法满足需求。

    培训公司数据库设计

    业务需求是这样的:

    每位学生每期只能参加一门课程。

    言外之意,公司有很多课程。我们只分析“每位学生每期只能参加一门课程”这一需求,发现涉及到两个实体:学生、课程。所以我们或许会想当然地这样去设计数据库:
    在这里插入图片描述
    一个课程可以由多个学生选择,一个学生只可以选择一门课程。发现问题了吗?业务需求里不是说一个学生只能参加一门课程,而是说一个学生在一期只能参加一门课程!这么设计数据库是在断人家财路。因此,我们必须考虑“每期课程”这个概念:在这里插入图片描述
    看样子似乎是没问题了,但是数据库设计是不可能这么容易就没问题的。我们把每期课程都作为一个记录,那么对于课程的信息,比方说课程名称、价格、介绍,每开一期课就要向数据库中存一行记录,因此我们的数据库会出现大量冗余(也就是说不满足数据库第二范式)。因此,我们应该这样去设计数据库:
    在这里插入图片描述
    看到了吗?这里的“Record”是一个弱实体,它的主键是“学期主键+学生主键”,代表学生参加课程这一行为,抽象成为了弱实体。为什么要用学期表的主键和学生表的主键呢?因为一个学生、一个学期,那么就只能参加一门课程了,所以根据主键唯一标识每行记录的原则,应该这样去选取。课程表的主键成为了Record表的外键,课程表与Record表之间存在一对多关系。

    在这个例子中,学生、课程是业务需求描述中显而易见的实体,“期”也可以认为是比较明显的实体,但“参加”这个动词在我们的数据库中便成为了“参加记录” ,也就是Record实体。

    教务系统数据库设计(二)

    这一部分的业务需求是这样的:

    老师授课。

    似乎业务需求很简单,但事实上,多位老师可以独立上同一门课,也可以共同上同一门课。一位老师可以参与多门课的授课。真实的教务系统的确是这个样子的。一般,像马原、高数等课程是多位老师独立授课,专业核心课大多是多位老师共同为同一班级授课。那么数据库要怎样设计呢?
    在这里插入图片描述
    像这样吗?老师、课程之间建立多对多关系?不难发现,这样的多对多联系无法区分上面所说的两种授课情况。也就是说,我们必须引入第三个表,才有可能实现业务需求,两张表格是行不通的。我是这样设计的:
    在这里插入图片描述
    在课程表与老师表之间,引入了新的一张表格——排课表。一个课程可以安排多次排课,一个排课就对应一个独立的授课安排,可以由一位老师完成,也可以由多位老师完成。那么像马原授课这种多位老师独立授课的情况该如何解决呢?在排课表中,认为不同老师的马原课是同一课程(引用Course表中同一记录的主键作为外键),但是它们是不同的排课(ArangeNo不同,可能一个是1,另一个是2)。也正因如此,排课表的主键是组合主键(课程编号+排课编号,如CS163、1)。

    这个例子中,需求中并没有提“排课”这一实体,这个实体完全是我们为了满足需求而定义的,甚至它和课程表在概念上还有点关系。注意到了吗,这里的“关系”就是弱实体概念中所说的“依赖”!

    四、 对强实体与弱实体的总结

    1. 区别弱实体与强实体的关键在于主键,“依赖”的实质是主键之间的关系。所以归根到底,就一个主键之间是否有关系、主键是否是组合主键的问题。

    2. 弱实体与强实体可以相互转换,没有绝对意义上的强与弱。既然区别弱实体与强实体的关键在于主键,那么一个同样意义的表,当我给它一个编号作为主键,那么它就不是弱实体,而如果我令它的主键是组合主键,它就是弱实体。就像刚刚,我们说排课表的主键是组合主键(课程编号+排课编号,如CS163、1),所以它是弱实体,那么如果我定义排课编号是“CS16301”,而不再是“1”,那么它的主键(排课编号)就不再需要课程编号,它就成为了强实体。

    3. 弱实体也可以依赖于弱实体。就像第一个例子中的Session,它依赖于Day,Day就是一个弱实体。

    4. 弱实体与它所依赖的实体之间的关系只能是1:1或n:1。也就是说,一个弱实体实例不可能依赖于同一实体的多个实例。这个其实很好理解,因为如果弱实体实例A依赖于实例B,那么A的主键要包括B的主键,所以A当然不可以依赖于很多个B。

    5. 业务需求决定弱实体的定义,分三种情况:

      情况一、 业务需求中明确的弱实体
      情况二、 业务需求中隐含的弱实体
      情况三、 业务需求中无、但为实现业务需求不得不定义的弱实体

      如果觉得这篇文章对你有帮助,请给博主点个赞!

    展开全文
  • 【命名实体识别(NER)】(1):命名实体识别综述

    万次阅读 多人点赞 2019-03-23 09:41:44
    命名实体识别综述 什么是命名实体识别? 命名实体识别(Named Entity Recognition,简称NER),又称作**“专名识别”,是自然语言处理中的一项基础任务,应用范围非常广泛。命名实体一般指的是文本中具有特定意义...

    什么是命名实体识别?

    命名实体识别(Named Entity Recognition,简称NER),又称作“专名识别”,是自然语言处理中的一项基础任务,应用范围非常广泛。命名实体一般指的是文本中具有特定意义或者指代性强的实体,通常包括 人名地名机构名日期时间专有名词等。通常包括两部分:

    • 实体的边界识别
    • 确定实体的类型(人名、地名、机构名或其他)

    NER系统就是从非结构化的输入文本中抽取出上述实体,并且可以按照业务需求识别出更多类别的实体,比如产品名称、型号、价格等。因此实体这个概念可以很广,只要是业务需要的特殊文本片段都可以称为实体。

    学术上NER所涉及的命名实体一般包括3大类(实体类,时间类,数字类)7小类(人名、地名、组织机构名、时间、日期、货币、百分比)

    实际应用中,NER模型通常只要识别出人名、地名、组织机构名、日期时间即可,一些系统还会给出专有名词结果(比如缩写、会议名、产品名等)。货币、百分比等数字类实体可通过正则搞定。另外,在一些应用场景下会给出特定领域内的实体,如书名、歌曲名、期刊名等。

    命名实体识别的价值和应用领域

    命名实体识别是NLP中一项基本性的关键任务,是关系抽取事件抽取知识图谱信息提取问答系统句法分析机器翻译等诸多NLP任务的基础,被广泛应用在自然语言处理领域,同时在自然语言处理技术走向实用化的过程中占有重要地位。

    • 事件检测: 地点、时间、人物是事件的基本构成部分,在构建事件的摘要时,可以突出相关人物、地点、单位等。在事件搜索系统中,相关的人物、时间、地点可以作为索引关键词。通过事件的几个构成部分之间的关系,可以从语义层面更详细地描述事件。
    • 信息检索: 命名实体可以用来提高和改进检索系统的效果,当用户输入“重大”时,可以发现用户更想检索的是“重庆大学”,而不是其对应的形容词含义。此外,在建立倒排索引的时候,如果把命名实体切成多个单词,将会导致查询效率降低。此外,搜索引擎正在向语义理解、计算答案的方向发展。
    • 语义网络: 语义网络中一般包括概念实例及其对应的关系,例如“国家”是一个概念,中国是一个实例,“中国”是一个“国家”表达实体与概念之间的关系。语义网络中的实例有很大一部分是命名实体。
    • 机器翻译: 命名实体的翻译(尤其像人名、专有名词、机构名等),常常会有某些特殊的翻译规则(例如中国的人名翻译成英文时要使用名字的拼音来表示,有名在前姓在后的规则),而普通的词语要翻译成对应的英文单词。准确识别出文本中的命名实体,对提高机器翻译的效果有直接的意义。
    • 问答系统: 准确识别出问题的各个组成部分问题的相关领域相关概念是问答系统的重点和难点。目前,大部分问答系统都只能搜索答案,而不能计算答案。搜索答案进行的是关键词的匹配,用户根据搜索结果人工提取答案,而更加友好的方式是把答案计算好呈现给用户。问答系统中有一部分问题需要考虑到实体之间的关系,例如“美国第四十五届总统”,目前的搜索引擎会以特殊的格式返回答案“特朗普”。

    命名实体识别的研究现状和难点

    命名实体识别当前并不是一个大热的研究课题,因为学术界部分学者认为这是一个已经解决了的问题,但是也有学者认为这个问题还没有得到很好地解决,原因主要有:

    • 命名实体识别只是在有限的文本类型(主要是新闻语料中)和实体类别(主要是人名、地名)中取得了效果
    • 与其他信息检索领域相比,实体命名评测语料较小,容易产生过拟合
    • 命名实体识别更侧重高召回率,但在信息检索领域,高准确率更重要
    • 通用的识别多种类型的命名实体的系统性很差。

    同时,中文的命名实体识别与英文的命名实体识别相比,挑战更大,目前未解决的难题更多。英语中的命名实体具有比较明显的形式标志,即实体中的每个词的第一个字母要大写,所以对实体边界的识别相对容易,任务的重点是确定实体的类别。和英语相比,汉语命名实体识别任务更加复杂,而且相对于实体类别标注任务而言,实体边界的识别更加困难。汉语命名实体识别的难点主要存在于:

    • 汉语文本没有类似英文文本中空格之类的显式标示词的边界标示符,命名实体识别的第一步就是确定词的边界,即中文分词
    • 汉语分词和命名实体识别互相影响
    • 除了英语中定义的实体,外国人名译名地名译名是存在于汉语中的两类特殊实体类型
    • 现代汉语文本,尤其是网络文本,常出现中英文交替使用,此时汉语命名实体识别的任务还包括识别其中的英文命名实体
    • 不同的命名实体具有不同的内部特征,不可能用一个统一的模型来刻画所有的实体内部特征
    • 现代汉语日新月异的发展给命名实体识别也带来了新的困难:(1)标注语料老旧,覆盖不全。譬如说,近年来起名字的习惯用字与以往相比有很大的变化,以及各种复姓识别、国外译名、网络红人、流行用语、虚拟人物和昵称的涌现。(2)命名实体歧义严重,消歧困难

    命名实体识别解决方案

    命名实体识别(NER)一直是NLP领域中的研究热点,从早期基于词典和规则的方法,到传统机器学习的方法,到近年来基于深度学习的方法,命名实体识别(NER)研究进展的大概趋势大致如下图所示:
    在这里插入图片描述

    基于规则和字典的方法

    基于规则的方法多采用语言学专家手工构造规则模板,选用特征包括统计信息标点符号关键字指示词和方向词位置词(如尾字)中心词等方法,以模式和字符串相匹配为主要手段,这类系统大多依赖于知识库和词典的建立。基于规则和词典的方法是命名实体识别中最早使用的方法,它们依赖于手工规则的系统,都使用命名实体库,而且对每一个规则都赋予权值。当遇到规则冲突的时候,选择权值最高的规则来判别命名实体的类型。一般而言,当提取的规则能比较精确地反映语言现象时,基于规则的方法性能要优于基于统计的方法。但基于规则和字典的方法也有其缺陷:

    • 规则往往依赖于具体语言、领域和文本风格,制定规则的过程耗时且难以涵盖所有的语言,特别容易产生错误,系统可移植性差,对于不同的系统需要语言学专家重新书写规则
    • 代价太大,存在系统建设周期长、需要建立不同领域知识库作为辅助以提高系统识别能力等问题

    基于统计机器学习的方法

    基于统计机器学习的方法主要包括:隐马尔可夫模型(Hidden Markov Moder, HMM)最大熵模型(Maximum Entropy Model, MEM)支持向量机(Support Vector Machine, SVM)条件随机场(Conditional Random Field, CRF)等等。在基于机器学习的方法中,NER被当作序列标注问题。利用大规模语料来学习出标注模型,从而对句子的各个位置进行标注。NER 任务中的常用模型包括生成式模型HMM、判别式模型CRF等。条件随机场(Conditional Random Field,CRF)是NER目前的主流模型。

    条件随机场(CRF) 的目标函数不仅考虑输入的状态特征函数,而且还包含了标签转移特征函数。在训练时可以使用SGD学习模型参数。在已知模型时,给输入序列求预测输出序列即求使目标函数最大化的最优序列,是一个动态规划问题,可以使用维特比算法进行解码。CRF的优点在于其为一个位置进行标注的过程中可以利用丰富的内部及上下文特征信息。
    在这里插入图片描述

    在传统机器学习方法中,常用的特征如下:
    在这里插入图片描述

    四种学习方法的优缺点分析

    • 最大熵模型结构紧凑,具有较好的通用性,主要缺点是训练时间复杂性非常高,有时甚至导致训练代价难以承受,而且最大熵模型需要归一化计算,导致开销比较大
    • 条件随机场为命名实体识别提供了一个特征灵活、全局最优的标注框架,但同时存在收敛速度慢、训练时间长的问题
    • 一般说来,最大熵和支持向量机在正确率上要比隐马尔可夫模型高一些,但是隐马尔可夫模型在训练和识别时的速度要快一些,主要是由于在利用Viterbi算法求解命名实体类别序列的效率较高
    • 隐马尔可夫模型更适用于一些对实时性有要求以及像信息检索这样需要处理大量文本的应用,如短文本命名实体识别

    基于统计的方法对特征选取的要求较高,需要从文本中选择对该项任务有影响的各种特征,并将这些特征加入到特征向量中。依据特定命名实体识别所面临的主要困难和所表现出的特性,考虑选择能有效反映该类实体特性的特征集合。主要做法是通过对训练语料所包含的语言信息进行统计和分析,从训练语料中挖掘出特征。有关特征可以分为具体的单词特征上下文特征词典及词性特征停用词特征核心词特征以及语义特征等。基于统计的方法对语料库的依赖也比较大,而可以用来构建和评估命名实体识别系统的大规模通用语料库又比较少,这是此种方法的又一大制约。

    基于深度学习的方法

    NN/CNN-CRF模型

    《Natural language processing (almost) from scratch》 是较早使用神经网络进行NER的代表工作之一。在这篇论文中,作者提出了窗口方法与句子方法两种网络结构来进行NER。这两种结构的主要区别就在于窗口方法仅使用当前预测词的上下文窗口进行输入,然后使用传统的NN结构;而句子方法是以整个句子作为当前预测词的输入,加入了句子中相对位置特征来区分句子中的每个词,然后使用了一层卷积神经网络CNN结构。
    在这里插入图片描述

    在训练阶段,作者也给出了两种目标函数:

    • 词级别的对数似然,即使用softmax来预测标签概率,当成是一个传统分类问题
    • 句子级别的对数似然,其实就是考虑到CRF模型在序列标注问题中的优势,将标签转移得分加入到目标函数中。后来许多相关工作把这个思想称为结合了一层CRF层,所以我这里称为NN/CNN-CRF模型。
      在这里插入图片描述

    在作者的实验中,上述提到的NN和CNN结构效果基本一致,但是句子级别似然函数即加入CRF层在NER的效果上有明显提高。

    RNN-CRF模型

    借鉴上面的CRF思路,出现了一系列使用RNN结构并结合CRF层进行NER的工作。模型结构如下图:
    在这里插入图片描述
    它主要有Embedding层(主要有词向量,字符向量以及一些额外特征),双向RNN层,tanh隐层以及最后的CRF层构成。它与之前NN/CNN-CRF的主要区别就是他使用的是双向RNN代替了NN/CNN。这里RNN常用LSTM或者GRU。实验结果表明RNN-CRF获得了更好的效果,已经达到或者超过了基于丰富特征的CRF模型,成为目前基于深度学习的NER方法中的最主流模型。在特征方面,该模型继承了深度学习方法的优势,无需特征工程,使用词向量以及字符向量就可以达到很好的效果,如果有高质量的词典特征,能够进一步获得提高。

    近期工作

    Attention-based

    《Attending to Characters in Neural Sequence Labeling Models》 该论文还是在RNN-CRF模型结构基础上,重点改进了词向量与字符向量的拼接。使用attention机制将原始的字符向量和词向量拼接改进为了权重求和,使用两层传统神经网络隐层来学习attention的权值,这样就使得模型可以动态地利用词向量和字符向量信息。实验结果表明比原始的拼接方法效果更好。
    在这里插入图片描述
    另一篇论文《Phonologically aware neural model for named entity recognition in low resource transfer settings》,在原始BiLSTM-CRF模型上,加入了音韵特征,并在字符向量上使用attention机制来学习关注更有效的字符,主要改进如下图:
    在这里插入图片描述

    少量标注数据

    对于深度学习方法,一般需要大量标注数据,但是在一些领域并没有海量的标注数据。所以在基于神经网络结构方法中如何使用少量标注数据进行NER也是最近研究的重点。其中包括了迁移学习《Transfer Learning for Sequence Tagging with Hierarchical Recurrent Networks》和半监督学习。这里我提一下最近ACL2017刚录用的一篇论文《Semi-supervised sequence tagging with bidirectional language models》。该论文使用海量无标注语料库训练了一个双向神经网络语言模型,然后使用这个训练好的语言模型来获取当前要标注词的语言模型向量(LM embedding),然后将该向量作为特征加入到原始的双向RNN-CRF模型中。实验结果表明,在少量标注数据上,加入这个语言模型向量能够大幅度提高NER效果,即使在大量的标注训练数据上,加入这个语言模型向量仍能提供原始RNN-CRF模型的效果。整体模型结构如下图:
    在这里插入图片描述

    总结

    对于统计机器学习方法而言,将 NER 视作序列标注任务,利用大规模语料来学习出标注模型,从而对句子的各个位置进行标注。常用的应用到 NER 任务中的模型包括生成式模型HMM、判别式模型CRF等。比较流行的方法是特征模板 + CRF的方案。

    随着硬件能力的发展以及词的分布式表示(word embedding)的出现,神经网络成为可以有效处理许多NLP任务的模型。通过将神经网络与CRF模型相结合的NN/CNN/RNN-CRF模型成为了目前NER的神经网络领域的主流模型。基于神经网络结构的NER方法,继承了深度学习方法的优点,无需大量人工特征。只需词向量和字符向量就能达到主流水平,加入高质量的词典特征能够进一步提升效果。

    近年来,命名实体识别在多媒体索引、半监督和无监督的学习、复杂语言环境和机器翻译等方面取得大量新的研究成果。随着半监督的学习和无监督的学习方法不断被引入到这个领域,采用未标注语料集等方法将逐步解决语料库不足的问题,将会成为未来NER研究领域的热点和重点。

    在后续博客中,我会陆续更新利用HMM、CRF、神经网络+CRF的解决方案解决中文领域命名实体识别任务。请随时关注。

    参考资料

    展开全文
  • 业务逻辑详解

    千次阅读 2018-05-29 19:00:13
    比如让你实现一个功能,给你两个数,让你获取它的和,你所写的如何才能获得任意给定的两个数的和,这个程序实现过程即可成为业务逻辑处理。“一个人了解的业务逻辑越多越细,他就是越好的需求分析师。”难题:什么是...
  • 命名实体识别(NER)发展简史

    千次阅读 2020-01-10 09:55:48
    在NLP的关键性基础任务—命名实体识别(Named Entity Recognition,NER)的研究中,深度学习也获得了不错的效果。最近,笔者阅读了一系列基于深度学习的NER研究的相关论文,并将其应用到达观的NER基础模块中,在此...
  • 数据库实体联系模型与关系模型

    千次阅读 2020-03-02 19:11:33
    (3)用菱形表示实体间的联系,菱形内写上联系名,用连接线把菱形分别与有关的实体相连接,在连接线旁标上连接的类型,如果联系也属性,则联系的属性和菱形连接。      图 2-7 用ER图表示的课程管理 ...
  • 介绍多表映射单实体和单表映射多实体业务场景。 Entity Framework 4.0中多表映射单实现和表映射多实体的实现方法。 对于复杂的可扩展的业务来说,实体和表不一定是一一对应的,一个数据表或能对应多个实体或多个...
  • 命名实体识别(NER)

    千次阅读 2018-10-27 22:07:54
    NER系统就是从非结构化的输入文本中抽取出上述实体,并且可以按照业务需求识别出更多类别的实体,比如产品名称、型号、价格等。实体这个概念可以很广,只要是业务需要的特殊文本片段都可以称...
  • 先说说业务规则。笔者习惯将业务规则分为三种。一种是全局规则,这种规则一般与所有用例都相关而不是与特定用例相关,例如actor要操作用例必须获得相应的...时候,这类规则也被写到软件架构文档中。关于用例补充规约
  • hibernate实体映射之讲解

    千次阅读 2015-09-13 11:53:41
    实体类与数据库之间存在某种映射关系,Hibernate依据这种映射关系完成数据的存取,因此映射关系的配置在Hibernate中是关键的。Hibernate支持xml配置文件与@注解配置两种方式。xml配置文件是基础的配置,而@注解...
  • 中国实体经济大调查

    千次阅读 2017-05-19 09:24:32
    前言 一业繁荣,万业凋敝。...樱桃小房子经过一个多月的多方调查了解发现,实体产业抵挡不住挣快钱的诱惑,也在务虚,能够洁身自好,坚持做实业的企业,却要在夹缝中求生存。 尤其是企业裁员降薪现象
  • 什么是业务逻辑?

    万次阅读 多人点赞 2018-09-04 20:32:07
    比如让你实现一个功能,给你两个数,让你获取它的和,你所写的如何才能获得任意给定的两个数的和,这个程序实现过程即可成为业务逻辑处理。       “一个人了解的业务逻辑越多越细,他就是越好的需求分析师。...
  • 十大赚钱增值业务排行榜

    千次阅读 2006-09-14 08:49:00
    市场描述:从2000年5月17日中国移动正式开通短信服务以来,手机用户开始了除语音以外的另一种全新的交流方式。与语音业务相比,短信的资费更为低廉,收费模式也很得人心(单向收费),而且操作简便,可谓“老少皆宜...
  • 实体类与数据库之间存在某种映射关系,Hibernate依据这种映射关系完成数据的存取,因此映射关系的配置在Hibernate中是关键的。Hibernate支持xml配置文件与@注解配置两种方式。xml配置文件是基础的配置,而@注解...
  • 建立实体-关系模型(案例)

    千次阅读 2019-04-25 15:10:06
    不仅仅是实体有属性,关系同样也有属性,这些属性在实体间建立关系时才会存在。 有时属性太多,无法在图上一一列出,可以用表格,在后面的步骤中这个表格同样会用到,如下: 四、确定属性域   属性域就是属性的...
  • 业务建模 之 业务序列图(一)

    千次阅读 2018-02-12 17:29:24
    http://blog.csdn.net/huaishu/article/details/39249383上...4.1 描述业务流程的手段描述业务用例的实现,即业务流程,几种可选的做法:【选择一】文本例如针对财务部“员工→报销”用例的实现,书写业务用例规约...
  • .net中关于实体类的映射的详解

    千次阅读 2018-05-14 11:28:43
    在实际的业务环境中,我们的Source类和Destination类的字段不可能一对一的匹配,这个时候我们就需要来指定他们的实际映射关系,例如: public class CalendarEvent { public DateTime Date { get ; set ; } ...
  •  对于一些业务逻辑可能出现在多个数据表中,建议封装成Business Rule,这样便于业务逻辑的重新使用,也便于业务逻辑的维护。  为了便于维护业务逻辑,可以考虑将Business Rule和Domains结合起来使用。将业务...
  • 在NLP的关键性基础任务—命名实体识别(Named Entity Recognition,NER)的研究中,深度学习也获得了不错的效果。最近,笔者阅读了一系列基于深度学习的NER研究的相关论文,并将其应用到达观的NER基础模块中,在此...
  • 聚合根、实体、值对象的区别?

    千次阅读 2018-07-06 17:17:33
    从标识的角度:聚合根具有全局的唯一标识,而实体只有在聚合内部唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法;从是否只读的角度:聚合根除了唯一标识外,其他所有状态信息都理论上可...
  • 怎样成为一个优秀的架构师?

    万次阅读 多人点赞 2019-10-08 17:15:37
    架构师是一个既能掌控整体又能洞悉局部瓶颈并依据具体的业务场景给出解决方案的团队领导型人物。看似完美的“人格模型”背后,是艰辛的探索。 架构师不是一个人,他需要建立高效卓越的体系,带领团队去攻城略地,在...
  • 实体关系对于一个数据库系统来说至关重要,而且EF的各个实体之间的联系,实体之间的协作,联合查询等也都依赖于这些实体关系。 一、前言 经过EF的《第一篇》,我们已经把数据访问层基本搭建...
  • 第3章 业务建模 之 业务用例图

    万次阅读 2018-02-12 17:56:19
    http://www.uml.org.cn/oobject/201409112.asp3.1 软件是组织的零件业务建模的...做一做以下题目,可能会发现答案出乎你的意料。很多时候我们把自己在开发的系统(现在流行叫××平台了)看得太重了,感觉没有我们开...
  • 自定义实体类简介 [收藏]

    千次阅读 2006-01-05 11:42:00
    自定义实体类简介 Karl SeguinMicrosoft Corporation摘要:有些情况下,非类型化的 DataSet 可能并非数据操作的最佳解决方案。本指南的目的就是探讨 DataSet 的一种替代解决方案,即:自定义实体与集合。(本文包含...
  • 使用自定义实体类和集合代替DataSet

    千次阅读 2011-07-01 09:47:00
    掌握 ASP.NET 之路:自定义实体类简介 发布日期: 5/24/2005 | 更新日期: 5/24/2005Karl SeguinMicrosoft Corporation摘要:有些情况下,非类型化的 DataSet 可能并非数据操作的最佳解决方案。本指南的目的就是...
  • 第一部分 To B or not to ...B端产品即要符合商业组织的战略要求,能够满足商业用户需求,将已商业运行逻辑进行系统化、信息化、高效化处理。两类都是为企业流程效率服务,让分散的、低效的个体,更好地连接合作,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 66,578
精华内容 26,631
关键字:

业务实体最有可能成为