作者:Dino Esposito
2001 年 4 月 26 日
最近,我投身于向大众介绍 ADO.NET 的工作,这有点传播福音的味道。在争论 ADO.NET 对象模型内在优点的同时,我发现在理解其与 ADO 的相似性问题上存在一道几乎难以逾越的鸿沟,后者就是来自此类对象模型。

大多数问题集中在 ADO.NET 的主要改进上。
断开连接的计算模型?“您的意思是说此模型可确保实现较高的 Web 应用程序可伸缩性级别吗?可是,从 ADO 2.0 时起就已经实现了这一目标。”
与 XML 更紧密的集成?“您在开玩笑,对吗?XML 和 ADO 已经在 ADO 2.1 中得到了首次集成,并在 ADO 2.5 中得到了进一步的加强。事实上,这是非常成功的集成。”
与框架其他部分的无缝集成?“您在讨论哪种框架?您指的是 COM 还是 COM+,抑或是 MFC 呢?”
不过,正是由于“框架”一词的出现,才第一次成功地击碎了时常包裹在 ADO.NET 演示过程之外“带有冷淡色彩”(即使不是“及其冷漠”)的坚冰。
这并不意味着,开发人员真的不关心一般意义上的数据访问以及特定意义上的 ADO.NET。他们更愿意了解新增的强大功能和现有服务令人吃惊的功能增强。而要使用 ADO.NET 进行编程,开发人员最终就得使用针对数据提供程序的连接和命令。这些连接和命令提取静态的记录快照,并要求运行库在他们完成操作时重新连接并提交更改。这些连接和命令坚持使用 XML 流传输其内容。
因此,人们难免要问 ADO.NET 提供了哪些新功能。当首次介绍 ADO.NET 而揭开其神秘面纱时,人们认为实际上它并没有什么新东西的潜意识也日渐增强。
再者,框架一词真正使人们对此持有的明显和固执的怀疑第一次出现了动摇,ADO.NET 演示者可以很容易看到这一点。
ADO.NET 与周围系统 SDK(此处为 .NET 框架)的集成是前所未有的。此类无缝集成包括基类库的设计模式和类型系统以及 Web 服务和 ASP .NET。无论您采用什么样的编程模型,只要使用了 .NET,ADO.NET 类就是首选的数据访问方式。
在上一期的专栏中,我介绍了数据读取器 — 即,一种简单而有效的向前游标,用于浏览数据行集。在本期专栏中,我重点介绍 DataSet 对象,这种 .NET 类是断开连接和 XML 驱动的数据容器的精髓。
数据集和记录集对象
本期的标题极其准确地描述了 DataSet 对象。如果在其前面再加上定语“内存中的”,它听起来就更加准确了。因此,如果加上该定语,您应该将 DataSet 对象视为内存中的、类似数据库的数据容器。
可以填充 DataSet,作为对 OLE DB 或托管提供程序运行查询命令的结果。尽管 DataSet 对象与 ADO 记录集具有不同的语法和结构,但至少在最简单的环境中最终使用 DataSet 对象的方式与使用 ADO 记录集是基本相同的。创建 DataSet 对象,设置一些参数,然后通过执行特殊的命令(通常为 SQL 命令)对它们进行数据填充。接下来,您在记录表中向前或向后移动记录指针,并更新或删除记录。
到目前为止,这实际上与 ADO 记录集并没有什么不同。而且,看不出在对象模型进行此类“变革”有什么必要。由于在 ADO.NET 中没有记录集对象,因此在现有的 ADO 代码中大大削弱了向后兼容性。但是,总而言之,正是由于没有记录集对象,您才可以大大优化数据访问组件的设计和性能。
让我们先分析 ADO Recordset和 DataSet 对象之间的类似之处。两者都是断开连接的,其自身都要序列化为 XML,并且都可以使用断开连接的数据手动进行构造。两者都支持数据构形、本地排序、筛选和批处理更新。
但也正是由于很多设计方面的问题而使它们具有明显的差异。数据集以数据为中心,而记录集却是以数据库为中心的。数据集仅在内存中工作,它们的存在与数据库或数据源(通常情况下)没有任何联系。DataSet 对象是内存中的表库;您可以使用所需数量的表,而不像在记录集中只能使用一个。
此外,DataSet 的内容可以序列化为 XML,而无需创建磁盘文件。在 ADO 中,XML 只是一种输出格式;而在 ADO.NET 中,XML 是对象的基本表示形式。可通过特定的一组属性和方法,随时提供 DataSet 内容的 XML 表示形式。在运行库中,无法通过怪异的 XML DOM 对象实例来生成这种表示形式。
数据集可以使用不同的数据架构来创建其 XML 输出,或者从远程源中重新生成。分层 XML 模型和关系记录集架构可以在 DataSet 对象环境中融洽地共存,以给您提供强大的记录集浏览功能。
最后,DataSet 对象是 .NET 托管类的实例,而不是 COM 对象的实例(通常是 COM 自动化对象的实例)。这意味着,可以将 DataSet 对象安全地存储在 ASP .NET 会话时隙中,而不必担心应用程序的整体可伸缩性。此外,可以通过网络传输它所包含的记录块,而无需考虑其中的公司防火墙。这是因为 .NET 对象是线程安全的并且不要求线程关系,而且,还因为数据集已经是 XML 流(可以通过 HTTP 和端口 80 安全地传输这种流)。
数据集对象剖析
DataSet 对象是从 Component 继承并实现 IListSource 接口的类。在 Visual Basic .NET 中,该声明显示如下:
Public Class DataSet Inherits Component Implements IListSource
反之,IListSource 接口由 IList 派生,并从代码所连接的某种数据源中获取其内容。IListSource 使用称为 GetList 的单一方法来公开此类内容。GetList 返回一个基于 IList 的项目集合。IList 是基本 ICollection 接口的子代,并且是所有 .NET 列表的抽象基类。
总而言之,DataSet 对象是保留数据的更专用对象的集合。当您需要将更多相关数据表保存在一个地方时,数据集本身按照给定的 XML 架构和非常有帮助的综合特性提供了进行序列化的能力。
此外,数据集是完全在内存中的对象,从正反两方面看,它通常都是适用于此角色的对象。只要存储的信息量不是特别易失的,或者其增长速度没有快到需要经常进行数据刷新的程度,就可以方便而快速地对它进行访问。
数据集是一个类似于数据库的容器。您处理的所有数据都保存在内存中,但它为您提供了索引、排序、逻辑合并、设置约束和检查数据完整性的方法。其中只有一些扩展功能是由 DataSet 对象直接实现的,此对象增强了整个 .NET 框架中的总体集成和一致性功能。
可以使用普通的 new 关键字及几个可能的构造函数来创建 DataSet 对象。
DataSet data = new DataSet(); DataSet data = new DataSet("MyDataSet");
正如您所看到的一样,不同之处只是您要为新创建的数据容器所分配的名称。要么省略名称,要么它必须是非空的名称。如果省略名称,并因而使用以前的构造函数,则名称默认为 NewDataSet。但是,数据集的名称在对象使用方面并未起到应有的作用。使用它的唯一地方是提供数据集内容的 XML 文档的根标记。
数据集对象地理学
让我们分析一下数据集的地理学(即,构成实际编程模型的子对象和集合),并讨论用于填充 DataSet 对象的各种方法。
数据集类包含三个主要集合:
• |
Tables |
• |
Relations |
• |
ExtendedProperties |
Tables 搜集您应该从其外部源获取的所有子数据表,并显式地将它们添加到库中。表是通过 DataTable 对象提供的。在成为数据集系列的一部分后,表可自动通过数据集的 XML 架构进行序列化。可通过唯一的名称来标识数据集中的任何表,但也可以通过索引来访问该表。
关系是一个逻辑 SQL JOIN 语句,它在先前添加到数据集中的两个表之间建立关系。关系将两个表链接到同一列中相匹配的值,其方式与 JOIN 语句基本相同。
数据集关系和 JOIN 语句的最大差别在于链接中涉及的两个表是不同的,并且不会创建任何统一的物理或逻辑表。可以将关系视为添加到主表行中的额外列,其内容是目标表中相匹配行的数组。
ExtendedProperties 获取自定义用户信息的集合,其结构和逻辑完全由您来确定。您使用此属性的方式与使用 ASP Session 或 Application 集合完全相同。
data.ExtendedProperties.Add("refreshat", "12:00");
在 DataSet 对象的其他属性当中,需要特别提及的是 DefaultView 属性。它返回 DataSetView 对象,该对象表示当前构成数据集的所有表的自定义筛选视图。可通过此功能生成数据集内容的多个不同的视图;例如,为不同用户显示不同字段的视图。
要设置数据集的视图,首先要创建并填充数据集。然后,通过将数据集引用传递到类构造函数来创建一个新的 DataSetView 对象实例。
DataSet data = new DataSet(); // fill the data set here DataSetView dsv = new DataSetView(data);
到目前为止,您已在两个对象之间建立了关联,但尚未提供创建表特定的视图集合所需的信息。您是通过新类型的对象(TableMapping 对象)完成此操作的。
表映射是指定义用于查看 DataSetView 视图中表的自定义设置。基本上,它在数据集中任何表上应用某种掩码。此掩码可以为字段提供自动筛选、排序以及备用命名功能。只需将 DataSetView 对象分配给数据集的 DefaultView 属性,就可以启用相同内容的不同视图。
数据集地理学中另外两个要素是 Xml 和 XmlData 属性。可通过它们,使用 XML 读取或更改数据集的结构和内容。Xml 属性同时公开架构和数据。XmlData 属性仅提供用于读取和写入的数据。
填充数据集
数据集是可以公开创建的对象,这几乎与所有其他 ADO.NET 对象是相同的。通常,您创建的数据集可以包含自定义名称,也可以不包含自定义名称。但是,您可以使用 DataSetName 属性获取和设置数据集的名称。然后,您可能希望设置某些环境属性,例如,CaseSensitive 和 EnforceConstraints。
前者决定子 DataTable 对象中的字符串比较是否必须区分大小写。默认情况下,此属性返回 False(假)。
EnforceConstraints 是一个布尔值,它表示在试图执行更新操作时,是否必须检查通过 DataTable 的 Constraints 集合设置的任何约束规则。Constraints 是 Constraint 对象的集合,其中的每个对象定义了表为保持数据完整性而强制实施的规则。
在数据集的 XML 方面,您可以决定设置在将对象的内容序列化为 XML 时要使用的命名空间名称(Namespace 属性)和命名空间的前缀(Prefix 属性)。
在创建后,数据集为空,因为其 Tables 集合没有元素。您可以使用两种基本方法向集合中添加表:通过 .NET 数据适配器,或者手动创建 DataTable 对象并将其添加到集合中。
数据适配器可以采用 SQLDatasetCommand 或 ADODatasetCommand 对象的形式。(注意,在 .NET 的 Beta 2 版本中,这些类的名称可能会有所不同。)
数据适配器公开隐藏以下内容的方法:内部实际发生并且与您的手动操作完全相同的情况。首先,按如下方式创建 DataTable 对象:
DataTable dt = new DataTable();
然后,按照列和相关属性为其指定一个架构,最后使用行来填充它。您使用 DataTable 的 Rows 集合完成此操作,并在另一个对象的后面附加一个 DataRow 对象。在表准备好之后,将其插入到 Tables 集合中。
数据适配器在 FillDataSet 方法的控制下执行此过程。
SQLDatasetCommand cmd = new SQLDatasetCommand(strCmd, strConn); DataSet data = new DataSet(); cmd.FillDataSet(data, strTableName);
此类方法的用途是,更新给定数据集中指定表的数据和架构。FillDataSet 使用您通过适配器的类构造函数传递的查询命令来从数据源中检索数据。
必须有与此命令关联的连接对象,对吗?可通过 SQLConnection 对象(如果针对的是 OLE DB 数据源,则为 ADOConnection 对象)或更简单的连接字符串来指定它。对于这两种方法,如果关闭了所需的连接,则将其打开以检索数据,然后再将其关闭。如果在调用 FillDataSet 前打开连接,则使用该连接并保持打开。
FillDataSet 具有额外的调用原型,该调用原型每次仅允许您加载所有选定记录的一部分。您指定要开始加载的记录的位置(从 0 开始)以及每个步骤要检索的最大记录数。通过此方法,能够实现从任何数据源中异步地读取记录。
再者,切记,如果此命令返回多个结果,FillDataSet 将仅考虑第一个结果;如果您惯常每次读取最大数量的记录,则只考虑结果的指定部分。相反,如果命令不返回任何行,则不会将任何表添加到数据集中。
在填充数据集时可能发生的错误是,它不能回滚已提交的更改。事实上,在出现错误之前添加或修改的任何行已被获取,并且无法再将其取消。当您的目标是刷新数据集(与填充数据集相对)时,如果您使用最初用于填充数据集的 SQL 语句并且主键信息存在,则可以避免重复的行。
如果您从关系数据库中提取记录,则表元数据通常引用主键信息。否则,可通过 DataTable 对象的 PrimaryKey 属性设置它。
DataColumn[] keys = new DataColumn[1]; DataTable dt = new DataTable("MyList"); keys[0] = dt.Columns["ID"]; dt.PrimaryKey = keys;
DataSet 对象具有恢复某些可恢复的丢失信息的内置功能。例如,MissingSchemaAction 属性表示如何管理潜在不一致的情况,其中丢失的表或列可能导致无法预知的行为。通过将预定义的值分配给 MissingSchemaAction,您要决定是必须将丢失的信息添加到数据集的架构中,还是发出警告或错误。如果您按以下方式将该属性设置为 AddWithKey:
data.MissingSchemaAction = MissingSchemaAction.AddWithKey
则要添加所有必需的列和键信息以完成架构。
数据适配器是最终创建和填充数据集的专用对象。您可以按照自己的控制来运行相同的过程,并使用非数据库记录来填充数据集。例如,以下片段显示如何添加具有目录信息的表:
DataTable dt = new DataTable(); DataColumn colName = new DataColumn(); colName.DataType = System.Type.GetType("System.String"); colName.ColumnName = "FolderName"; dt.Columns.Add(colName); DataColumn colDesc = new DataColumn(); colDesc.DataType = System.Type.GetType("System.String"); colDesc.ColumnName = "FolderDesc"; dt.Columns.Add(colDesc); Directory dir = new Directory(strDir); foreach (Directory d in dir.GetDirectories()) { DataRow dr = dt.NewRow(); dr["FolderName"] = d.Name; dr["FolderDesc"] = "Content of " + d.Name; dt.Rows.Add(dr); } DataSet data = new DataSet(); data.Tables.Add(dt);
无论它们的实际来源是什么,都可以通过相同的 API 来处理所有链接到 DataSet 对象上的表。就为表建立关系、建立索引或坚持使用 XML 而言,从 SQL Server 中创建表与通过扫描文件夹内容来创建表没有区别。
所有数据保存在内存中,并且可以对它们进行更新、排序和筛选,而无需使用任何服务器端的功能。所有类似于数据库的功能均实现为内存中的功能,其中包括与很多 DBMS 的事务处理模型非常相似的提交模型。当要保存对数据源所做的更改时,实际上只需要返回到服务器即可。这将提出另一种问题,我将在后面的一期专栏中对其进行讨论。
对话栏:爱你没商量 (.NET)
我觉得有一天我会喜欢上 ADO.NET 和 ASP .NET 的,但这一天还没有到来。实际上,我还没有到推荐在下一个项目中大胆试用它们的份上。理想情况下,我希望再等上至少一年以后再开始,并且等上两年左右再付诸实施。投入生产是要慎之又慎的。我会错过什么吗?
实际上,我们现在即将发行 .NET Beta 2 版。在我给您解答问题时,很多参与测试项目的公司正在“得心应手”地运用它。将其公开发行只需要几周的时间。在您拿到该产品时,一定要仔细阅读文档。您使用它越是得心应手,我越是建议您大胆在项目中试用一下。
当您编写代码时,系统文档是您的良师益友。如果您考虑将使用尚未完全掌握的新平台时,这些文档尤其有用。要顺利完成项目,您必须依靠团队的聪明才智并阅读相关的文档。
到第一次发行 .NET 平台时,我们无疑将提供非常精彩的文档。我们已准备好了非常精彩的 Beta 2 文档,这清楚地表明我们正朝着这一正确方向前进。
首先,我应该亲自查阅 ASP .NET 服务器控件的文档,尤其是自定义控件。然后,我应该确保相关对象(和相同方法的替代对象)的 ADO.NET 文档并不是巧妙地进行剪剪贴贴的产物。最后,我应该确保我有能力部署 ASP .NET 应用程序。
不能只因为喜爱 .NET 就开始一个实际的项目,但对 SDK 和文档感到得心应手则是一个良好的开端。这正是您开始新的“生产”活动所必需的。
Dino Esposito 是 Wintellect 的 ADO.NET专家兼培训教员和顾问,工作地点位于意大利的罗马。Dino 是 MSDN Magazine 的特约编辑,是 Cutting Edge 专栏的撰稿人。他还定期向 Developer Network Journal 和 MSDN News 投稿。Dino 是 Microsoft Press 即将出版的《Building Web Solutions with ASP.NET and ADO.NET》一书的作者,并且是 http://www.vb2themax.com/ 的创始人之一。如果希望与 Dino 联系,可发送电子邮件至:dinoe@wintellect.com。