ADO.NET 中的 XML 特性
Dino Esposito Wintellect
2001 年 12 月 13 日
毫无疑问,XML 及其一些相关技术(包括 XPath、XSL Transformation 和 XML Schema)是 ADO.NET 的基础。与 ADO 相比,ADO.NET 对象模型的互操作性得到很大的提高,而事实上,XML 正是为此发挥重要作用的关键元素。在 ADO 中,XML 只是一个用于保留断开连接的记录集的内容的(非默认)I/O 格式。而 XML 在 ADO.NET 的构建和交互方面的参与则深入得多。可以用以下几点来概括 ADO.NET 与 XML 的更强的交互和集成:
• | 对象序列化和远程处理 |
• | 双编程接口 |
• | XML 驱动的批处理更新(仅适用于 SQL Server 2000) |
在 ADO.NET 中,您可以用若干种方法将对象保存到 XML 文档以及从 XML 文档还原对象。总之,这种能力只属于 DataSet 对象,但可以用最少的代码扩展到其他容器对象。将诸如 DataTable 和 DataView 这样的对象保存到 XML 实质上可视为数据集序列化的特殊情况。
此外,ADO.NET 和 XML 类提供了一种统一的中间 API,程序员可通过同步的双编程接口来使用它。您可以使用 XML 基于节点的分层法或基于列的表格式数据集关系法来访问和更新数据。您可以在任何时候从数据的数据集表示形式切换到 XML DOM,反之亦然。数据将进行同步,而且您在其中一个模型中输入的任何更改会立即在另一个模型中有所反映,并可以看见。在本文中,我将讨论 ADO.NET 到 XML 的序列化和 XML 数据访问,也就是上面列表中的前两点。下个月,我将主要讨论 XML 驱动的批处理更新 — 您从 SQL Server 2000 XML Extensions (SQLXML 2.0) 获得的最酷特性之一。
数据集和 XML
就像任何其他 .NET 对象一样,DataSet 对象以二进制格式存储在内存中。不过,与其他对象不同的是,数据集始终以一种名为 DiffGram 的特殊XML 格式来进行远程处理和序列化。当数据集进入应用程序域的边界或计算机的物理边界时,会自动呈现为 DiffGram。在目标端,无提示地重新构建数据集为二进制对象,并可以立即使用。应用程序可以通过一组方法来使用同样的序列化功能,其中的一对方法显然非常突出。它们是 ReadXml 和 WriteXml。下表显示了您可以用来在读写中使用 XML 的数据集的方法。
GetXml |
| ||||
GetXmlSchema |
| ||||
ReadXml |
| ||||
ReadXmlSchema |
| ||||
WriteXml |
| ||||
WriteXmlSchema |
|
正如上表所示,使用 DataSet 和 XML 时,您可以将数据和架构信息作为不同的实体进行管理。您可以接受来自数据集的 XML 架构,将它作为字符串使用。您也可以将它写入磁盘文件或将它加载到一个空的 DataSet 对象。与上表中列出的方法可以相提并论的是,DataSet 对象还包含两个与 XML 相关的属性 — Namespace 和 Prefix。Namespace 确定将 XML 属性和元素读取到数据集时用于限定其范围的 XML 命名空间。作为命名空间别名的前缀存储在 Prefix 属性中。
从 XML 构建数据集
ReadXml 方法填充从多个源读取的 DataSet 对象,这些源包括磁盘文件、.NET 流或 XmlReader 对象的实例。该方法可以处理任何类型的 XML 文件,但是,如果 XML 文件具有构形相当不规则的非表格式结构,则在以列和行的格式呈现时,当然可能产生一些问题。
ReadXml 方法有几个重载,所有这些重载都十分类似。它们接受 XML 源以及可选的作为参数的 XmlReadMode 值。例如:
public XmlReadMode ReadXml(String, XmlReadMode);
该方法根据指定的读模式以及数据集中是否已存在架构来为数据集创建关系架构。下面的代码片段显示了用于从 XML 加载数据集的典型代码。
StreamReader sr = new StreamReader(fileName); DataSet ds = new DataSet(); ds.ReadXml(sr); // defaults to XmlReadMode.Auto sr.Close();
将 XML 源的内容加载到数据集时,ReadXml 不合并其主键信息相匹配的行。要将现有的数据集与从 XML 加载的数据集合并,首先必须创建一个新的数据集,然后使用 Merge 方法合并这两个数据集。在合并期间,所要覆盖的行就是那些具有相匹配的主键的行。您也可以使用另一种方法将现有的 DataSet 对象与从 XML 读取的内容进行合并,即通过 DiffGram 格式(稍后将详细讨论)。
下表解释了 ReadXml 支持的多个读模式。您可以使用 XmlReadMode 枚举对它们进行设置。
IgnoreSchema | 忽略任何嵌入式架构,而依靠数据集的现有架构 |
ReadSchema | 读取任何嵌入式架构,并加载数据和架构 |
InferSchema | 忽略任何嵌入式架构,并从 XML 数据推断架构 |
DiffGram | 读取 DiffGram,并将数据添加到当前架构 |
Fragment | 读取和添加 XML 片段,一直到流的结尾 |
默认的读模式没有在表中列出,这就是 XmlReadMode.Auto。设置此模式时,或者尚未显式设置任何读模式时,ReadXml 方法将检查 XML 源并选择最适合的选项。
如果发现 XML 源是 DiffGram,则将源作为 DiffGram 加载。如果源正好包含嵌入式架构或对外部架构的引用,将使用 ReadXmlSchema 来加载源。最后,如果 XML 源中不存在任何架构信息,则 ReadXml 方法使用数据集的 InferXmlSchema 方法推断架构。数据集的关系结构(即架构)由表、列、约束和关系组成。下面我们来看看设置其中的每一种模式时会发生什么情况。
XmlReadMode.IgnoreSchema 选项将使该方法忽略任何嵌入式架构或引用的架构。因此,该数据被加载到现有的数据集架构,而不适合的任何数据将被放弃。如果数据集中不存在架构,则不加载任何数据。注意,空的数据集没有架构信息。记住,如果 XML 源是 DiffGram 格式的,则 IgnoreSchema 选项的效果与 XmlReadMode.DiffGram 一样。
// no schema in the DataSet, no data will be loaded DataSet ds = new DataSet(); StreamReader sr = new StreamReader(fileName); ds.ReadXml(sr, XmlReadMode.IgnoreSchema);
XmlReadMode.ReadSchema 选项只对嵌入式架构有效,而不识别外部引用。它可以向数据集添加新表,但如果数据集中已存在在嵌入式架构中定义的任何表,则将引发异常。您不能使用 ReadSchema 选项来更改一个现有表的架构。如果数据集不包含架构(也就是说,数据集是空的),并且没有嵌入式架构,则不读取和加载任何数据。ReadXml 只能读取使用 XML 架构定义语言 (XSD) 或 XML-Data Reduced (XDR) 定义的嵌入式架构。不支持任何文档类型定义 (DTD)。
如果设置了 XmlReadMode.InferSchema 选项,ReadXml 将直接从 XML 数据的结构推断架构,并忽略可能存在的任何嵌入式架构。只有在推断出架构后,才加载数据。通过根据情况来添加新表或向现有的表添加新列可扩展现有的架构。您可以使用数据集的 InferXmlSchema 方法将架构从指定的 XML 文件加载到数据集。在某种程度上,您可以控制在架构推断操作过程中处理的 XML 元素。利用方法 InferXmlSchema 的签名,您可以指定一组其元素将从推断中排除的命名空间。
void InferXmlSchema(String fileName, String[] rgNamespace);
DiffGram 是 ADO.NET 用于保存数据集状态的 XML 格式。与 SQLXML 的 updategram 格式类似的是,DiffGram 同时包括数据行的当前状态和原始状态。使用 ReadXml 加载 DiffGram 时,将合并具有相匹配的主键的行。您可以使用 XmlReadMode.DiffGram 标志显式指示 ReadXml 对 DiffGram 生效。使用 DiffGram 格式时,目标数据集必须具有与 DiffGram 相同的架构,否则,合并操作将失败,并会引发异常。
如果设置了 XmlReadMode.Fragment 选项,将从 XML 片段加载数据集。XML 片段是一段有效的 XML,它标识元素、属性和文档。元素的 XML 片段是完全限定 XML 元素(节点、CDATA、处理指令、注释)的标记文本。属性的片段是属性值,文档的片段则是整个内容集合。如果 XML 数据是一个片段,则不应用完整格式的 XML 文档的根级别规则。与现有的架构相匹配的片段被追加到适当的表,不匹配架构的片段将被放弃。ReadXml 从当前位置读到流的结尾。XmlReadMode.Fragment 选项不应该用于填充一个空的而且缺少架构的数据集。
将数据集对象序列化到 XML
数据集的 XML 表示形式可以使用 WriteXml 方法写入文件、流、XmlWriter 对象或字符串。XML 表示形式可以包括架构信息,也可以不包括架构信息。WriteXml 方法的实际行为可通过您可以传递的 XmlWriteMode 参数来控制。XmlWriteMode 枚举中的值决定着输出的布局。数据集表示形式包括表、关系和约束定义。如果您不选择使用 DiffGram 格式,数据集的表中的行就只写入其当前版本。下表概括了 XmlWriteMode 可以使用的写选项。
IgnoreSchema | 将数据集内容作为没有架构的 XML 数据写入 |
WriteSchema | 写入具有嵌入式 XSD 架构的数据集内容 |
DiffGram | 将数据集内容作为 DiffGram 写入,包括原始值和当前值 |
XmlWriteMode.IgnoreSchema 是默认选项。下面的代码显示了将数据集序列化为 XML 的典型方式。
// ds is the DataSet StreamWriter sw = new StreamWriter(fileName); ds.WriteXml(sw); // defaults to XmlWriteMode.IgnoreSchema sw.Close();
有几个因素影响了从 DataSet 对象创建的 XML 文档的最终结构。这些因素包括:
• | 使用的 XML 整体格式 — DiffGram 或当前内容的无格式分层表示形式 |
• | 架构信息是否存在 |
• | 嵌套关系 |
• | 表列如何映射到 XML 元素 |
DiffGram 格式是本人稍后将进一步说明的特殊 XML 格式。它不包括架构信息,但保留行状态和行错误。因而,它似乎能构成数据集实时实例的更接近的表示形式。
架构信息如果存在于所创建的数据集中,则将始终作为嵌入式 XSD 写入。您不可能将它作为 XDR、DTD 写入,或添加对外部文件的引用。如果尚未为生成的 XML 文件的根节点指定名称,则接受数据集的名称或 NewDataSet。下面的代码片段是由两个表组成的 DataSet 对象的 XML 表示形式的示例,这两个表是 Customers 和 Orders,它们之间的关系是通过 CustomerID 字段形成的。
<MyDataSet> <xs:schema ... /> <Customers> <CustomerID>1</CustomerID> <FName>John</FName> <LName>Smith</LName> </Customers> <Customers> <CustomerID>2</CustomerID> <FName>Joe</FName> <LName>Users</LName> </Customers> <Orders> <CustomerID>1</CustomerID> <OrderID>000A01</OrderID> </Orders> <Orders> <CustomerID>1</CustomerID> <OrderID>000B01</OrderID> </Orders> </MyDataSet>
您很难根据上面列出的代码判断出这两个表之间有关系。有关这方面的一些信息设置在 <xs:schema>树中,但除此之外,没有任何其他信息有助于推断出该结论。如果将在 CustomerID 字段上设定的关系归纳成文字,则可表达为 — 某个给定客户发出的所有定单。上面的 XML 树不提供此信息的立即表示形式。当数据集中存在数据关系时,要更改节点的顺序,可以将 DataRelation 对象的 Nested 属性设置为 true。作为此更改的结果,从更改得出的 XML 代码如下所示:
<MyDataSet> <xs:schema ... /> <Customers> <CustomerID>1</CustomerID> <FName>John</FName> <LName>Smith</LName> <Orders> <CustomerID>1</CustomerID> <OrderID>000A01</OrderID></Orders><Orders> <CustomerID>1</CustomerID> <OrderID>000B01</OrderID></Orders> </Customers> <Customers> <CustomerID>2</CustomerID> <FName>Joe</FName> <LName>Users</LName> </Customers> </MyDataSet>
正如您看到的那样,所有定单现在集中在相应的客户子树下。
默认情况下,在 XML 表中,列呈现为节点元素。不过,这只是一个可以在每列基础上进行调整的设置。DataColumn 对象有一个名为 ColumnMapping 的属性,它决定了该列如何以 XML 形式呈现。ColumnMapping 属性接受下面列出的 MappingType 枚举中的值。
Element | 映射到 XML 节点元素: <CustomerID>value</CustomerID> |
Attribute | 映射到 XML 节点属性: <Customers CustomerID=value> |
Hidden | 不显示在 XML 数据中,除非使用的是 DiffGram 格式 |
SimpleContent | 映射到简单文本: <Customers>value</Customers> |
如果 XML 输出格式是 DiffGram,则忽略 Hidden 映射类型。不过,在这种情况下,列的 DiffGram 表示形式包含一个特殊属性,它将列标记为最初针对 XML 序列化隐藏起来。SimpleContent 映射类型并非始终可用,只有当表中有列的时候才可以使用。
DiffGram 格式
DiffGram 只是一个根据表示数据集内容的具体架构写入的 XML 字符串。它决不是 .NET 类型。下面的代码片段显示了如何将 DataSet 对象序列化到 DiffGram。
StreamWriter sw = new StreamWriter(fileName); ds.WriteXml(sw, XmlWriteMode.DiffGram); sw.Close();
产生的 XML 代码放在<diffgr:diffgram> 节点中,最多包含三个不同的数据节,如下所示:
<diffgr:diffgram> <MyDataSet> : </MyDataSet> <diffgr:before> : </diffgr:before> <diffgr:errors> : </diffgr:errors> </diffgr:diffgram>
DiffGram 的第一节是强制性的,表示数据的当前实例。它几乎与您可以从普通的序列化得到的 XML 输出完全相同。二者之间的主要区别是,DiffGram 格式从来不包括架构信息。
该数据节包括数据集中行的当前值。包括删除行在内的原始行存储在 <diffgr:before>节中。这里只列出了修改的记录或删除的记录。新添加的记录只在数据实例中列出,因为它们没有所要链接到的前述引用。这两节中的行是使用唯一 ID 进行跟踪的。这些行表示数据集的原始版本和当前版本之间的增量。
最后,在 <diffgr:errors>节列出与行上挂起的错误相关的任何消息。同样,在这种情况下,使用讨论是否要进行更改的相同的唯一 ID 来跟踪行。DiffGram 节点可以用特殊属性标记为跨不同的节(数据实例、更改和错误)使元素相关。
diffgr:hasChanges | 该行已被修改(请参见 <diffgr:before>中的相关行)或插入。 |
diffgr:hasErrors | 该行有错误(请参见 中的相关行)。 |
diffgr:id | 确定用于跨节耦合行的 ID:TableName+RowIdentifier。 |
diffgr:parentId | 确定用于标识当前行的父行的 ID。 |
diffgr:error | 包含 <diffgr:before>中的行的错误文本。 |
msdata:rowOrder | 跟踪数据集中的行的序号位置。 |
diffgr:hidden | 确定被标记为隐藏的 msdata:hiddenColumn= ???… 的列。 |
ADO.NET 框架只对 DataSet 对象提供显式 XML 支持。不过,将 DataView 或 DataTable 转换为 XML 并不是特别难。在这两种情况下,您都必须使用临时的数据集作为要另存为 XML 的行集的容器。用于将 DataTable 保存为 XML 所必需的代码很简单。
void WriteDataTableToXml(String fileName, DataTable dt) { // Duplicate the table and add it to a temporary DataSet DataSet dsTmp = new DataSet(); DataTable dtTmp = dt.Copy(); dsTmp.Tables.Add(dtTmp); // Save the temporary DataSet to XML StreamWriter sr = new StreamWriter(fileName); dsTmp.WriteXml(sr); sr.Close(); }
每个 ADO.NET 对象只能由一个容器对象引用,由于这个简单的原因,复制 DataTable 对象是非常重要的。您不能有相同的实例,例如,一个 DataTable 对象属于两个不同的 DataSet 对象。
与 DataTable 对象不同的是,DataView 不是数据集的标准组成部分,因此,为了将它保存到 XML,您应该将 DataView 转换为一个表对象。这个过程可用下面的代码片段来实现:
void DataViewToDataTable(DataView dv) { // Clone the structure of the table behind the view DataTable dtTemp = dv.Table.Clone(); dtTemp.TableName = "Row"; // this is arbitrary! // Populate the table with rows in the view foreach(DataRowView drv in dv) dtTemp.ImportRow(drv.Row); // giving a custom name to the DataSet can help to // come up with a clearer layout but is not mandatory DataSet dsTemp = new DataSet(dv.Table.TableName); // Add the new table to a temporary DataSet dsTemp.Tables.Add(dtTemp); }
第一个步骤是,对所处理的 DataView 对象后面的表的结构进行克隆。接着,遍历此视图中的所有记录,并将相应的行添加到临时的 DataTable。然后,将此 DataTable 添加到临时数据集,并对该 DataTable 进行序列化。还可以设法向数据集提供表名,并向整个 XML 输出提供自定义格式。例如:
<TableName> <Row> <Column1>??</Column1> : </Row> <Row> : </Row> <Row> : </Row> </TableName>
XmlDataDocument 类
XML 和 ADO.NET 框架为访问以 XML 和关系数据的形式表示的数据提供了统一的模型。其中,关键的 XML 类是 XmlDataDocument,而 DataSet 则是关键的 ADO.NET 类。具体来说,XmlDataDocument 从基类 XmlDocument 继承,并且只是在与 DataSet 对象保持同步的能力方面与该基类有所不同。进行同步时,DataSet 类和 XmlDataDocument 类的目标是同一个行集合,并且您能够通过两个接口(节点和关系表)来应用更改,还可以使这两个类可以立即看到更改。基本上,DataSet 和 XmlDataDocument 为同一数据提供两套方法。因此,您可以将 XSLT 转换应用于关系数据,通过 XPath 表达式查询关系数据以及使用 SQL 来选择 XML 节点。
您可以通过几种方法将 DataSet 对象和 XmlDataDocument 对象绑定在一起。第一个方法是,将一个非空的 DataSet 对象传递到 XmlDataDocument 类的构造函数。
XmlDataDocument doc = new XmlDataDocument(dataset);
与其基类相似的是,XmlDataDocument 提供了一种使用 XML 数据的 XML DOM 方法,因而,它与 XML 读取器和编写器非常不同。下面的示例显示了对这两个对象进行同步的另一种方法,这就是从 XML DOM 的一个非空实例创建一个有效的非空 DataSet 对象。
XmlDataDocument doc = new XmlDataDocument(); doc.Load(fileName); DataSet dataset = doc.DataSet;
您可以使用 XmlDataDocument 的 DataSet 属性将 XML 文档变成 DataSet 对象。该属性对 DataSet 对象进行实例化和填充,并返回该对象。在您第一次访问 DataSet 属性时,数据集就与 XmlDataDocument 相关联起来了。方法 GetElementFromRow 和 GetRowFromElement 在数据的 XML 形式和关系视图之间切换。为了从关系的角度查看 XML 数据,您必须先指定要用于数据映射的架构。可通过对同一个 XML 文件调用 ReadXmlSchema 方法来达到此目的。另一种方法是,您可以手动创建数据集中必需的表和列。
不过,还有一种同步 XmlDataDocument 和 DataSet 对象的方法,那就是当它们为空时,分别对其进行填充。例如:
DataSet dataset = new DataSet(); XmlDataDocument xmldoc = new XmlDataDocument(dataset); xmldoc.Load("file.xml");
将两个对象保持同步可以提供前所未有的灵活性,正如前面所提到的,您可以使用两个截然不同的导航类型来在记录之间进行移动。实际上,您可以对 XML 节点使用类似 SQL 的查询,以及对关系行使用 XPath 查询。
并非所有 XML 文件都可以成功地与数据集同步。为了保持同步,XML 文档必须有一个可以映射到关系结构的正规表格式结构,在关系结构中,每一行都有相同数量的列。XML 文档在呈现为 DataSet 对象时,将丢失任何 XML 特定的信息,这些信息可能是它们已经拥有的,并且没有关系上的对应部分。这些信息包括注释、声明和处理指令。
小结
在 ADO.NET 中,XML 不仅仅是用于对内容进行序列化的简单输出格式。您可以使用 XML 对 DataSet 对象的整个内容进行序列化,但您也可以选择实际的 XML 架构并控制所得到的 XML 文档的结构。您可以监视数据集的内容,包括表和关系,可以接受最终文档得出的架构信息,甚至还可以采用 DiffGram 格式。
当 ADO.NET 与 XML 进行交互和集成时,可以提供更多的特性。特别是,在 .NET 中,您可以同时提供和利用同一数据的两个同等但独立的视图,它们遵循着不同的逻辑数据表示形式。
对话栏:对批处理更新使用 GetChanges
我已经发现,数据集编程接口提供了名为 GetChanges 的方法,该方法返回一个更小的数据集,这个数据集只填充了所有包括的表中那些更新过的行。因此,这使我认为,使用这个更小的数据集而不是那个原始数据集可以提高性能。不过,您在上一篇文章中提到过某种情况,文章名称和出处我记不清了,说这种情况已引发出一些尚未确定的异常。因此,我的问题是,您是否能更清楚地说明一下数据集的 GetChanges 方法在批处理更新中的使用?
ADO.NET 批处理更新基于对指定表上的行进行遍历的循环。代码检查行的状态,并决定要执行哪个操作。该循环作用于您作为参数提供给适配器的方法 Fill 的数据集和数据表。如果您对原始数据集或对由 GetChanges 返回的更小的数据集调用 Fill,结果将大致相同。这样做将导致优化程度最低,并且只起到了缩小循环长度的作用。
在批处理更新过程中,数据行是按从中间层到数据服务器的顺序进行处理的。不存在一次性地或作为单一数据块发送到数据库的数据快照。实际上,在这种情况下,使用 GetChanges 将获得优化程度大得多的代码。
决定在批处理更新期间如何执行许多重要操作的关键参数是所修改的行的数量。无论您使用的是原始数据集还是由 GetChanges 返回的数据集,该参数都不更改。
相反,如果您对由 GetChanges 返回的数据集进行批处理更新,当检测到冲突时,您可能会遇到严重的问题。在这种情况下,在失败的行之前处理的行将正常提交,但它们不在原始数据集上!要确保应用程序的一致性,您必须接受提交的行上的更改,以及原始数据集上的更改。此代码完全是独立的。总而言之,如果您使用原始数据集,则批处理更新代码要简单得多。
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。