摘要:本文讨论如何以 ADO.NET 方式实现基本数据库操作,以及何时使用 ADO.NET 代替 ADO。
目录
.NET 中的数据访问
读取数据
DataSet、DataTable 和 Recordset
转换现有代码
更新数据
XML 扩展支持
总结
自若干年前推出开放式数据库连接 (ODBC) 应用程序编程接口 (API) 以来,出现了各种各样的数据库访问技术,而 ADO.NET 是其中最新的一种。在这过程中,发生了许多有趣的事。例如,COM 闯入数据库领域,开始培植 OLE DB 的殖民进程。然后,大致相当于 OLE DB 自动化版本的 ActiveX® Data Objects (ADO) 被选来统治 Windows® 数据库开发者的 Visual Basic® 和 ASP 社区。
通过 .NET,Microsoft 正在提供通用框架(即 Framework Class Library),其中将包括所有现有的 Windows API 甚至更多的内容。特别值得一提的是,它包括大量常用的库,而这些库现在需要通过各个 COM 对象分别获得。在这些库中,您会发现 XML 和 ADO 对象模型,它们被集成到了叫做 ADO.NET 的类子树中。
ADO.NET 事实上成为构建数据感知 .NET 应用程序的基础。和 ADO 不同的是,ADO.NET 遵循更通用的原则,不那么专门面向数据库。ADO.NET 集合了所有允许数据处理的类。这些类表示具有典型数据库功能(如索引、排序和视图)的数据容器对象。尽管 ADO.NET 是 .NET 数据库应用程序的权威解决方案,但从总体设计上来看,它不象 ADO 模型那样以数据库为中心,这是 ADO.NET 的一大特点。
ADO.NET 与 ADO 有很大差异。ADO.NET 是新的数据访问编程模型,需要开发人员的全面理解、投入和新思维。然而,一旦开始掌握 ADO.NET,您将意识到:原有的 ADO 技巧非常有助于您以不同、却更巧妙和可靠的方式来创建有效的应用程序和解决各种老问题。
在这篇文章的其余部分,我将集中介绍如何以 ADO.NET 方式实现基本的数据库操作。我想说明,在什么时候 ADO.NET 是比 ADO 更好的选择,而您最好在什么时候应放弃 ADO。ADO.NET 并不是将 ADO 改良以符合 .NET 基础结构而形成的。只要您看一下 ADO.NET 的语法、代码设计和移植,就会明白这一点。
.NET 中的数据访问
在 ADO.NET 中访问数据源的方式由托管提供程序确定。从功能上讲,托管提供程序与 OLE DB 的提供程序非常相似,但有两个重要的不同之处。首先,管理提供程序在 .NET 环境中工作,通过 DataReader 和 DataTable 等 .NET 类检索和公开数据。其次,因为它们的体系结构针对 .NET 进行了优化,所以比较简单。
目前 ADO.NET 提供了两种托管提供程序:一种用于 SQL Server™ 7.0 或更高版本,另一种用于其他所有您可能已经安装的 OLE DB 提供程序。在这两种情况下您分别使用不同的类,但遵循相似的命名规则。除前缀外,名称都是相同的。前一种情况前缀为 SQL,后一种情况则是 ADO。
您应该使用 SQL 类访问 SQL Server 表,因为它们直接进入数据库服务器的内部 API,跳过了由 OLE DB 提供程序表示的中间层。ADO 类是 OLE DB 提供程序上的 .NET 接口,它们使用 COM Interop 桥进行工作。
ADO.NET 对象的初学者可参阅 Omri Gazitt 的文章介绍 ADO+:用于 Microsoft .NET 框架的数据访问服务(英文)和我的 ADO+ 推动数据种类的演变(英文)一文。前者技术性较强,针对 ADO.NET 程序模型提供了高水平的评注性概述。后者主要介绍 ADO.NET 的目标和它与 XML、脚本以及其他技术之间的联系。
读取数据
需要从数据源中读取数据的 ADO.NET 应用程序首先要创建连接对象。根据目标提供程序的不同,该连接对象可以是 SQLConnection 或 ADOConnection。请记住,您可以使用 ADO.NET 类来连接到 SQL Server 数据库,但我们不建议这样做。其唯一的缺点是,您的代码要通过不必要的额外代码层。它先将 ADO 的托管提供程序调入,然后托管提供程序再调用 SQL Server OLE DB 提供程序。而 SQL Server 托管提供程序和 OLE DB 提供程序一样直接操作数据。
ADO 和 ADO.NET 连接对象之间的显著差异是:ADO.NET 连接不支持 CursorLocation 属性。请注意,这并不是一个文档错误,而是一个有争议的设计问题。为了突出以数据为中心的原则,ADO.NET 没有游标的显式实现。
在 ADO 中,您习惯了用游标从数据库或其他任何 OLE DB 兼容的数据源中抽取记录。您可以选择客户端或服务器游标,每种游标都有几个预先设定的游标类型。ADO.NET 则设计为从数据源中抽取数据,并提供新的编程接口来读取和分析数据。
在 ADO 中,您通过指定连接和命令文本来创建 Recordset 对象。对于游标的位置和类型,Recordset 有一定策略。您可以按下列方式之一读取数据:
在内存中创建选定记录的静态副本,然后在从数据源断开连接时根据需要处理这些记录。ADO 称之为静态游标。
通过快速、仅向前的只读游标来滚动数据,这种游标工作在记录的静态快照中。ADO 称之为只读游标。
通过服务器端的两种游标来访问数据,这些游标需要保持良好的连接,但您可以在各个不同层次上随时检测其他已连接的用户的更改。ADO 称它们为键集和动态游标。
前两种方式都在断开连接的记录集内工作,并从客户端缓存读取信息,这是它们的相似之处。另外,在面向 Web 的环境中和对于新的 n 层系统,这两种方式被证明是使用频率最高的。
在 ADO 中,以上所有这些方式与不同类型的游标相对应。您将在本文后面发现,虽然 ADO.NET 有很大不同,但它能实现您用 ADO 可实现的任何功能。只不过您的代码将从实际数据源及其物理存储媒介和格式中抽取数据。
ADO.NET 提供两个对象来处理从数据源中抽取的数据。它们是 DataSet 和 DataReader 对象。前者是记录在内存中的缓存,您可以从任何方向随意访问和修改。后者是高度优化的对象,专为以仅向前方式滚动只读记录而设计。请注意 DataSet 看起来象静态游标,但实际上,在 .NET 中与 ADO 只读游标相对应的是 DataReader 对象。
在 ADO.NET 中,不支持服务器端游标。然而,这不意味着您不能使用游标。您需要做的是在 .NET 中导入 ADO 类型库。在项目窗口的 References 节点上单击右键就行了。导入之后,您便可以开始在应用程序中使用本地 ADO 对象了。
尽管我承认下决心转向 .NET 是一件很难的事情,但我个人还是建议您考虑用 .NET 重写现有应用程序。可以把完全导入 ADO 作为迈向 .NET 的第一步,这无须投入太多的时间和资源。然而,请记住这只是漫漫长路上的第一步。这绝不是您迈向 .NET 的唯一一步。.NET 具有超值价值的的真正原因在于统一和一致的编程接口以及对本地类的广泛使用。您可以导入 COM 类型库,但导入 COM 类型库只能作为临时解决方案或者中间步骤,我们并不鼓励这样做。
使用 ADO.NET 时,应当充分考虑到它统一了数据容器类编程接口这一事实。无论您打算编写何种应用程序,Windows 窗体、Web 窗体还是 Web 服务,都可以通过同一组类来处理数据。不管在后端的数据源是 SQL Server 数据库、OLE DB、XML 文件还是一个数组,您都可以通过相同的方法和属性来滚动和处理它们的内容。
图 1:Solution Explorer 菜单
如果您坚持在 .NET 中使用 ADO,请准备面对一些副作用。例如,您需要额外的代码才能够从数据绑定控件中使用记录集。
DataSet、DataTable 和 Recordset
在 ADO.NET 中,没有与 Recordset 对象直接对应的对象。最接近的是 DataTable 对象。尽管这两个对象的功能几乎一样,但它们在各自的框架中起不同的作用。
Recordset 是一个大型对象,具有许多 ADO 功能,但还是有所欠缺。Recordset 在很多方面性能优良,例如可创建性、断开连接时仍能工作、功能丰富等等。但是,在某些方面仍然有待提高。例如,由于 Recordset 固有的 COM 特性,通过网络进行序列化的工作将非常繁重。又如它是二进制对象,所以在不同的平台上运行的模块很难共享它,而且它不能穿过防火墙。另外,Recordset 表示多个记录的单个表。如果该表是由一个或多个 JOIN 产生的,更新原始数据源可能会很困难。如果您要使断开连接的记录集和原始数据源保持协调,数据源必须能够识别 SQL。然而,您的记录集很可能是通过非 SQL 提供程序创建的。
在 ADO.NET 中,ADO Recordset 的所有功能被拆分成几个较简单的对象,DataReader 就是其中之一。DataReader 模拟快速、仅向前的只读游标的操作。
DataTable 是一个表示数据源的简单对象。您可以手动构造 DataTable,也可以通过 DataSet 命令自动填充它。DataTable 不区分它所包含的数据的来源。该对象允许您在内存中处理数据,以及执行浏览、排序、编辑、应用筛选器、创建视图等操作。
ADO 中没有与 DataSet 相对应的对象。DataSet 对象是一个容器类,是实现 ADO.NET 数据抽取的关键对象。DataSet 将一个或多个 DataTable 对象分组。DataTable 通过象行和列这样的通用集合公开它的内容。当您尝试从数据表中读取数据时,您可能会经过两个不同的对象层:DataTableMapping 和 DataView。
DataTableMapping 对象描述了数据源中的数据列和 DataTable 对象之间的映射关系。当填充 DataSet 时,DataSetCommand 对象要使用这个类。它维护数据集中的抽象列和数据源中的物理列之间的链接。
表的视图通过 DataView 对象实现。它表示 DataTable 的自定义视图,可以绑定到特定控件(如 Windows 窗体和 Web 窗体中的数据网格)中。该对象相当于 SQL CREATE VIEW 语句在内存中的实现。
DataSet 中的所有表都可以通过一个公用域放入关系中。这个关系由 DataRelation 对象管理。这看起来很象 ADO 的数据形成,但有一点重要区别。您不需要使用数据形成语言,您最终会拥有一个非常灵活的结构体系。ADO.NET 导航模型使您可以轻而易举地从某一张表内的主行移入它的所有子行。
DataRelation 对象相当于 JOIN 语句在内存中的实现,可用于建立数据类型相同的列的父/子关系。一旦建立了关系,就不允许出现任何会破坏这种关系的更改,如果出现就会导致运行时异常。视图和关系是实现主表/明细表架构的两种方式。要记住,视图只是放在记录上的掩码,而关系是设置在两个表的一个或多个列之间的动态链接。如果使用关系,您不能更改顺序或设置条件。
如果您的代码需要一对一外键关系,并且不更改数据,那么您最好不要使用无格式的 JOIN 命令。如果您需要额外的筛选功能,就应该使用 ADO.NET 自定义视图。
转换现有代码
有许多 ASP 页面使用 ADO 对象来抽取数据。让我们来讨论几种典型的情况,您在不久的将来移植和改编代码时可能会遇上这些情形。
如果您有从单个记录集生成报表的 ASP 页面,DataReader 对象将是您最好的伙伴。
您浏览 DataReader 对象时,它会将结果输出到页面。
String strConn, strCmd;
strConn = "DATABASE=MyAgenda;SERVER=localhost;UID=sa;PWD=;";
strCmd = "Select * From Names where ID=" + contactID.Text;
SQLConnection oCN = new SQLConnection(strConn);
SQLCommand oCMD = new SQLCommand(strCmd, oCN);
oCN.Open();
SQLDataReader dr;
oCMD.Execute(out dr);
while (dr.Read()) {
// 使用 dr.GetString(index) 或
// dr["field name"] 的方法 Response.Write 来输出数据
}
您还可以用 HasMoreRows 属性快速检查 DataReader 是否为空。如果您只需要快速浏览一系列记录,没有比 DataReader 更好更快的对象了。它同样适用于查询单个记录。您不能编辑 DataReader 的内容,但您可以将其内容移入更易于管理的对象,例如 DataTable 或者一个或多个 DataRow 对象。
当您需要处理表和记录之间的复杂关系时,DataReader 就不再是合适的工具了。在 ADO 中, 最终您需要处理记录集。您的数据模型链接越多,SQL 命令就越复杂。导航模型仍然是顺序的,最后放入缓存的数据往往多于你所需要的。DataSet 和 DataRelation 对象是这种表关系模型的基础。
为了管理父/子关系,ADO 还封装了数据形成引擎。从功能上讲,数据形成和 ADO.NET 关系是一样的。然而,从设计方面来看,它们几乎没有什么共同点。形成记录集将所有信息嵌入单个列表对象。ADO.NET 关系是您可以随时在两个数据表之间建立的动态链接。为了在执行单个 ADO 命令的过程中创建一个层次结构记录集,ADO 要依靠 Shaping OLE DB 服务提供程序,并且使用特定的类 SQL 语言。
在 ADO.NET 中,关系中涉及的每个对象总是被看成单独的个体。关系本身作为对象被公开,并且具有一定的行为规则。例如,DataRelation 对象可以从父行到子行一层层进行更改。您可以通过将 ForeignKeyConstraint 对象添加到 DataTable 的 Constraints 集合中来进行此操作。ForeignKeyConstraint 对象表示当删除或更新数值和行时,对通过外键关系相关联的一组列的约束。如前面提到的,一旦设置好了关系,在它按程序预设终止之前,您不能进行可能破坏该关系的更改。
另外,关系是不可传递的。您可以建立两组不同的关系,例如客户和订单、订单和产品之间的关系。然而,当在订单中导航以寻找某一位客户时,您不能从一个订单跳到与之相关的产品行。您必须另外打开订单/产品关系,定位到您需要的订单,然后才能获取相关的行。这就是为什么有时候最好不要通过原来的无格式 SQL JOIN 语句实现一对一关系的原因。
需要在 ASP Session 对象中存储记录吗?利用 ADO.NET 和 DataSet 对象,您可以相当安全的操作而不会导致在在 GIT 中存储 ADO 记录集可能会导致访问冲突(英文)中所讨论的问题,也不会有线程相似性的麻烦。
更新数据
更新数据时,Web 应用程序通常使用无格式 SQL 语句,或者使用更好的参数化存储过程。然而,当需要使用未连接的数据时,您可能想使用内置服务来更新所有需要修订的记录。ADO 提供了批更新机制来实现这个功能。
UpdateBatch 方法用于把保存在副本缓冲中的 Recordset 更改发送到服务器,以更新数据源。它采用开放式锁定,允许所有挂起的本地更改。它还在单个操作中把所有更改传送到数据源。仅当更改提交后数据源锁定要更改的记录时,才会出现开放式锁定。开放式锁定使两个用户可以同时访问同一个记录,但一个用户输入的更改很快会被另一用户所覆盖。当然,这种方式要求数据源能够检测和防止数据冲突。还要求整个数据源比较稳定,不会发生频繁的更改。否则,不难想象协调费用将很快超过替代严格锁定所带来的节约。事实上,使用 UpdateBatch 方法,在任何更改失败时都会返回一个错误。然后,您可以通过 Errors 集合和 Error 对象来访问该错误。
要理解 ADO.NET 模型为什么是更新数据的更强大的工具,理解 ADO 中开放式锁定的工作原理是非常关键的。在 ADO 代码中,您无法控制调用 UpdateBatch 之后所发生的一切。也就是说,更新是在服务器上通过滚动已更改的行,然后比较原始值和数据源中对应记录中的当前值来进行的。当所有的值都一致了,才对表执行适当的 SQL 语句(INSERT、UPDATE 或 DELETE)。
问题在于您不能控制实际应用于更改的 SQL 语句。服务器端的更新代码并不比您编写的代码好,如果您采用非 SQL 提供程序,它甚至无法运行。在本节的开头,我曾讲过 Web 应用程序通常通过参数化存储过程来更新数据。然而,如果您使用批更新就不同了。
在 ADO.NET 中,这个模型已经有所扩展。现在它采用更通用的架构,允许您自己指定基本操作命令,例如插入、删除、更新和选择等。其用意很明显:不论何种数据源,都可以从中抽取数据并提供同样的支持。在 ADO.NET 中进行批更新,您需要创建 DataSetCommand 对象即 SQLDataSetCommand 或 ADODataSetCommand。
注意:在 Beta 2 中,DataSetCommand 对象将被称为 DataAdapter 对象。
拥有 DataSetCommand 对象之后,您便可以调用它的 Update 方法。DataSetCommand 提供 InsertCommand、DeleteCommand、UpdateCommand 和 SelectCommand 等属性。它们都是 Command 对象。但是,除非默认行为无法满足需要,否则您不必设置它们。这与在 ADO 中一样。在 Update 过程中,如果没有设置任何 xxxCommand 属性,但是存在主键信息,将自动生成 Command 对象。请注意,要使上述过程正确进行,必须为所涉及的数据表设置主键。
以下代码显示了如何为 DataSet 的 EmployeesList 表设置主键:
DataColumn[] keys = new DataColumn[1];
keys[0] = m_oDS.Tables["EmployeesList"].Columns["EmployeeID"];
m_oDS.Tables["EmployeesList"].PrimaryKey = keys;
主键基本上是 DataColumn 对象的一个数组。
如果您要使用存储过程来更新表,或者采用专用非 SQL 数据提供程序,您会经常用到这些命令属性。
XML 扩展支持
在 ADO 中,XML 只不过是输入和输出格式。然而在 ADO.NET 中,XML 是一种数据格式,提供了操作、组织、共享和传递数据的手段。任何带入 DataSet 的数据,无论其来源,都能通过双面编程模型进行处理。您可以顺序交替访问信息,或者按行访问,也可以按照 XML 文档对象模型驱动的非顺序、层次结构路径进行访问。
DataSet 将数据和架构作为 XML 文档进行读写。数据和架构都可以通过 HTTP 传输,并且能在所有支持 XML 的平台上使用。相同的数据在不同的时候可以通过不同的架构来呈现,这是通过 XSLT 实现的。您可以使用 ReadXmlSchema 方法编写架构。XML 架构包括数据集中的表的说明,以及表的关系和约束。在调用 ReadXmlData 方法填充 DataSet 之前,应该先完成这个步骤。
以下代码示例是一个显示可更新数据表的最简单的 ASP.NET 页面。
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.IO" %>
<script runat="server" language="C#">
void Page_Load(Object source, EventArgs e)
{
DataSet data = new DataSet();
// 加载 XML 数据和架构
StreamReader sr;
sr = new StreamReader(Server.MapPath("data.xml"));
data.ReadXml(sr);
sr.Close();
// 添加通过 URL 传递的新记录
if (Request.QueryString.Count >0)
{
DataTable dt = data.Tables[0];
DataRow dr = dt.NewRow();
dr["FirstName"] = Request.QueryString["First"];
dr["LastName"] = Request.QueryString["Last"];
dt.Rows.Add(dr);
dt.AcceptChanges();
StreamWriter sw;
sw = new StreamWriter(Server.MapPath("data.xml"));
data.WriteXml(sw);
sw.Close();
}
// 刷新 UI(由网格组成)
grid.DataSource = data.Tables[0].DefaultView;
grid.DataBind();
}
</script>
如图 2 所示,您可以将新的行添加到表中。然而,它不涉及 SQL Server 或 Access 表。它只是一个 XML 文件,在处理它的代码中,没有使用 XML 节点或 XMLDOM 方法。您可以用相同的直观数据表接口来读取和更新 XML 记录。您的工作方式与在 ADO 中大致相同,但此处的模型更深入、更庞大,有更多的潜力供您去发掘。
图 2:可更新表的示例
总结
Web 应用程序的成功改变了典型分布式系统的面貌。现在大多数分布式系统都是 n 层系统,这类系统对扩展性和互操作性的要求越来越高。因此,非连接数据处理和 XML 成为最佳实践,并为业界广为接受。
ADO.NET 尝试将当今一些最好的实践统一在 .NET 下。这种用于数据访问的编程模型全面而又非常强大。但这个模型可能尚不能满足每一个人的要求,在将来的模型设计中还需要迈出一大步。然而,请记住现在 ADO.NET 还只是 Beta 版,只有有限的文档支持。
ADO 程序员从 Beta 版中获益最多,因为他们熟悉了 ADO.NET 的许多方面,包括最高层次的抽象即启发性模型。ADO.NET 代码与现有的 ADO 代码不兼容,但功能相似。要充分利用 ADO.NET,您应该花些功夫来理解概念本身,而不仅仅是找出移植代码的最快方式。无论您选择何种 .NET 编程模型,Windows 窗体、Web 窗体还是 Web 服务,ADO.NET 都会帮助您处理好数据访问的问题。
……