数据读取器揭密

在谈到 ADO.NET 时,您可能很难会不提到 ADO。如果没有其他原因的话,您最后提及 ADO 只是因为其名称在某些用于数据访问的 .NET 专用类中是硬编码的。 在最简单的形式下,ADO.NET 就是其名称所代表的含义:适用于 .NET 的 ADO。尽管此定义是正确的,但的确需要对它进行进一步的解释。 ADO 和 ADO.NET 具有不同的设计中心,但两者都试图提供相同的逻辑功能集。两者在实现此功能时,都遵循其自己的设计模式。因此,您具有两个 有点脱节的 API,因而会出现向后兼容性问题。 但是,除了语法和语义上的直接差别外,您还必须认识到这是最新 Microsoft 数据访问策略的共同基础。 在上一期专栏中,我解释道,按照 UDA 自然法则,.NET 托管提供程序是 OLE DB 的达尔文进化过程的下一阶段。类似地,ADO.NET 是 ADO 自然 进化的下一阶段 — 按照达尔文的观点,进化始终是有目的性的。 进化是关于对新环境的适应情况。ADO 的新环境是 .NET,因此,它需要修改其行为和属性以适应新的计算情况。ADO 并不是唯一进行此类进化变 革的软件技术。OLE DB、COM(+)、MSMQ 和 ASP 也都要经历这一过程。

使用 ADO 读取记录

随着时间的推移,当相互连接并且(大多数)为两层的应用程序向多层和基于 Web 的结构转变时,ADO 出现了,并获得了巨大的发展。整体 ADO 对 象模型设计反映了这种转变过程。 Web 强制要求采用断开连接的模型,在该模型下,在客户端上下载和缓存越来越多的信息。在 ADO 内,您可以发现隐藏在 Recordset 对象外表后面的两个核心内容。Recordset 可以利用服务器游标,并且随时都能够完成连接到数据源所需的所有操作。同时,同一对象的 另一方面可以在断开连接时工作,并以两种方式生成正确的记录表示形式。它可以从数据源中提取行,然后断开连接,或者从客户端磁盘文件中读取所需的 所有信息(包括具有固定架构的 XML 文件)。

ADO 的断开连接的功能已固化到 Recordset 对象接口上,要特别注意可通过等效的 API 提供连接的和断开 连接的功能。因此,Recordset 变得难以运用,而并不是充满魅力,在使用时需要为其提供重要的支撑操作。

总之,可以在 ADO 中以三种方式获取数据:

1.通过使用服务器游标的全时连接。

2.通过专用于读取和处理一组记录的快速、只读和只进的机制。

3.获取数据的静态快照,在与数据源断开连接的情况下处理它们,并随后提交更改。

无论采取何种工作方式,您始终使用 Recordset 对象,并通过选择正确的游标类型和位置来选择操 作模式。(有关详细信息,请参见 ADO 文档。)

这里有一个明显的缺点,那就是,无论如何优化 Recordset 对象和完善其设计,您在内存中加载的内容总比实 际需要的要多。此外,它添加了额外的代码层以处理输入参数,并可能会修正参数值。考虑到 ADO 的总体编程接口,该代码层是必需的,尽管其会影响性能。 ADO.NET 依照古老的“分而治之”格言而简化了数据读取基础结构。它引入了两个新对象,它们没有人们特别熟悉的 名称,但的确公开一些非常知名的功能。它们是 DataReader 和 DataSet。 DataReader 对象是 ADO.NET 版本的只读、只进的默认 ADO 游标。DataSet 是 一个容器,可以以编程方式填充静态数据快照。从这种意义上讲,可以将它视为断开连接的记录集库。ADO.NET 对象模型中没有记录集组件,但 DataTable对象是 .NET 版本的断开连接的记录集。 在 ADO.NET 中,没有提供对服务器游标的显式支持,但在将来的版本(在数据提供程序是 SQL Server 7.0 或更高版本时)中,您应该可以看到 ADO.NET 会支持 服务器游标。 如果您现在需要服务器游标,只需在 .NET 应用程序中导入 ADO 类型库并通过它进行编码即可。要了解有关它的优点、缺点和不利因素的详细信息,请参见上一期 的专栏文章ADO 在 .NET 应用程序中的妙用。

ADO.NET 是围绕 XML 进行设计的,以便在高度互操作和断开连接的环境中无缝地进行工作。从这种观点看,对于大部分人员目前编写和规划的应用程序来说,它无 疑比 ADO 更适合。它简化了编码,并优化了具有基于 Web 的客户端的数据访问组件的工作。 当支持 Web 的数据库服务器(例如,SQL Server 2000)具有 XMLhttp 支持时,它也具有良好的性能。顺便说一下,不要忘了查对 XML for SQL Server 2000 的最 新Web版本1,它改进了对插入、更新、删除以及将大型 XML 文件加载到 SQL Server 表中的支持。 尽管存在实际的语法差别,也要尽可能地将 ADO 和 ADO.NET 背后的编程哲学保持一致,这样可以缩短数据访问开发人员的学习周期和减少他们必须掌握的新概念的 数量。

>ADO 存在的迹象

时常,您可以在很多 ADO.NET 代码片段片段中看到 ADO 存在的反常迹象。您想看一个示例吗?让我们考虑一个 ADO 代码片段片段,该片段片段隐藏在成千上万 个 ASP 页和中间层组件当中,尽管其风格略有不同。

Set oCN = Server.CreateObject("ADODB.Connection")
oCN.Open strConn
Set oCMD = Server.CreateObject("ADODB.Command")
Set oCMD.ActiveConnection = oCN
Set oRS = oCMD.Execute(strCmd)
			

在此片段中,您显式创建一个 Connection 和一个 Command 对象,在执行该代 码片段之后,将返回一个新的 Recordset 对象。Connection 对象包含有关所需游标类 型和位置的信息。如果您选择 adOpenStatic 类型的游标,并且必然是客户端位置,则可以安全地关闭连接,并继续浏览记录。否则,在浏览完所提取的行之前,必须将连 接保持打开。 在获取记录集后,您可以使用循环来浏览它,如下所示:

While Not oRS.EOF
Response.Write(oRS("lastname") & "
")
oRS.MoveNext
Wend
			

在 ADO.NET 中,这种类型的代码不会按原样运行。然而,可通过 ADO.NET DataReader 对象来编写代码,至少从功能上 说,这种代码与原始代码几乎相同。

首先,创建一个特殊的对象来控制命令的执行。基本的 .NET 类是 DBCommand。通常不使用此类;应该使用更专用的 .NET 类(例如,ADOCommand 和 SQLCommand)或任何由用户定义的派生类。DBCommand 表示一个数据源可以理解的命令。 SQLCommand 类嵌入了 SQL Server 命令的功能,同时 ADOCommand 包装 COM 组件针对任何 OLE DB 提供程序所进行的任何访问。这些对象具有相同的编程接口和大量的构 造函数。经常使用的一个对象如下所示:

Dim oCMD As New SQLCommand(strCmd)
			

您还需要将连接对象与该命令关联起来。与 ADO 一样,可以显式和隐式完成此操作。对于前一种情况,通常创建一个 ADOConnection 或 SQLConnection 对象,具体情况取决于您针对的是哪种数据提供程序。ADOConnection 通过 COM Interop 桥梁找到 OLE DB 提供程序,而 SQLConnection 则连接到 SQL Server 注册。

Dim oCN As New SQLConnection(strConn)
			

通过对基于 DBCommand 的对象使用不同的构造函数,可以强制 ADO.NET 类创建将通过 DBCommand 的 ActiveConnection 属性公开 的隐式连接对象。

Dim oCMD As New SQLCommand(strCmd, strConn)
oCMD.ActiveConnection.Open()
			

不言自明,通过使用显式连接对象,可以在代码中重复使用同一个对象实例。您可以使用同一个对象变量连接到不同的数据源,这样可略微减少应用程序的内存遗迹。 无论使用哪种方法创建连接对象,切记必须始终显式打开它,并随后将其关闭。

oCMD.ActiveConnection.Open()

如果要启动一个存储过程,可通过设置 CommandType 属性来让 ADO.NET 运行库知道这一情况。

oCMD.CommandType = CommandType.StoredProcedure
			

与前面讨论的 ADO 代码匹配的 ADO.NET 代码片段片段类似于以下内容:

Dim oCN As New SQLConnection(strConn)
Dim oCMD As New SQLCommand(strCmd, oCN)
Dim oDR As SQLDataReader = Nothing
oCMD.ActiveConnection.Open()
oCMD.Execute(oDR)
While(oDR.Read)
	Response.Write(oDR["lastname"].ToString() + "
	")
End While
			

尽管存在具有不同名称以及不同方法和属性集的对象,逻辑架构与 ADO 中的情况非常相近。 在此 ADO.NET 片段中,虽然 SQLDataReader 对象缺少很多记录集功能,但它起到了 ADO 记录集的作用。在以只进方式浏览数据方 面,SQLDataReader 的效果非常好。但是,对于此类小功能,您现在有了相应的小对象。 此外,SQLDataReader 对象具有更佳的程序员友好性,因为它在读取后会自动将记录指针向前移到一个位置。换句话 说,Read 方法包含了对记录集的 MoveNext 方法的等效方法的调用。请考虑以下情况:默认位置 在流的开头并在第一条记录之前。在访问任何数据字段之前,您始终需要调用 Read。 总之,应该重新编写所有现有的 ADO 代码,除非您要搭建一个通向该代码的 COM 桥梁。这样一来,最大限度地减少了要了解的新概念,同样地,ADO 开发人员在编写 ADO.NET 代 码时也可以做到事半功倍。

ADO.NET 数据读取器

在 ADO.NET 中,您可以使用两种类型的数据读取器对象:SQLDataReader 和 ADODataReader。两者都 派生于 DBDataReader — 一个在 System.Data.Internal 命名空间中定义的抽象类。 此类定义了组件的非常基本的行为,这些组件通过连接绑定到数据源,并使您可以向前浏览一组提取的行。下图说明了 ILDASM 如何呈现类接口。

ms810288.data04122001-fig01(zh-cn,MSDN.10).gif

图 1. DBDataReader 类

正如前面提到的一样,.NET 框架从 DBDataReader 中创建两个类:ADODataReader 和 SQLDataReader。前者管理通过 OLE DB 路由的数据访问。而后者针对 SQL Server 托管提供程序。

以下代码片段片段显示在 C# 中如何声明这两个类:

public sealed class ADODataReader : DBDataReader, IDataReader,
IDataRecord 
public sealed class SQLDataReader : DBDataReader, IDataReader,
IDataRecord, ISQLDataRecord
			

注意,使用新的 sealed 属性来密封类以防止继承。

为完整起见,让我们看一下相同声明在 Visual Basic .NET 中是什么情况:

NotInheritable Public Class ADODataReader
Inherits DBDataReader
Implements IDataReader, IDataRecord
NotInheritable Public Class SQLDataReader
Inherits DBDataReader
Implements IDataReader, IDataRecord, ISQLDataRecord
			

Visual Basic .NET 声明可提供更多的信息,因为它将不同的关键字用于逻辑上不同的任务:Implements 和 Inherits。它清楚地说明数据读取器类是从非常小的 DBDataReader 类继承的,但实现了两个或多个特定的接口。 不能从 ADODataReader 和 SQLDataReader 中继承。到底是什么原因呢? 数据读取器只是一些对象,它们提供从某种数据源快速读取只进数据记录流的机制。无论是什么数据源,数据读取器对象都应该公开相同的、不变的编程接口。允许进一步继 承可能会导致专用数据读取器在编程接口中隐藏某些功能。 如果您需要为自定义数据源编写数据读取器,这种设计非常有用。您只需从 DBDataReader 中继承并实现几个接口即可。 如果您需要修改浏览 SQL Server 表的方式,则可以始终使用聚合功能。基本上,您创建一个数据读取器类,它在内部承载 标准 SQLDataReader 对象的实例,并利用此类方法来配备其自己的数据读取器接口。

ms810288.data04122001-fig02(zh-cn,MSDN.10).gif

图 2. 使用聚合从密封类“继承”

数据读取器类之所以独一无二且非常特殊,都归因于其所公开的一组接口。IDataReader 和 IDataRecord 就是其中的接口。此外,SQL Server 数据读取器实现ISQLDataRecord, ISQLDataRecord ?éò?公开 SQL Server 特定的数据类型。

到现在为止,您应该清楚为什么数据读取器是非常特殊的对象。并不是因为它们提供的功能比其他 .NET 类多或者少,而是因为它们遵循特定的设计规则。例如,只能通过 由 DBCommand 派生的对象的 Execute 方法来创建数据读取器。但是,不能通过直接使用该构造函数来创建它们。需要重申的是,必须从它 们在 ADO.NET 结构中所起到的作用上找原因。数据读取器必须是连接的 数据使用者和特殊数据提供程序之间 的不变 接口。 为什么您需要利用此类不变接口(就像在 OLE DB 中一样,听起来有点耳熟吗?请参见有关 OLE DB 和 .NET 的思考data03222001)来提取记录呢?因为数据读取器以连接方式 进行工作!事实上,数据集(断开连接的对象)并不是密封类,构造函数可以对其进行构造。 在使用数据读取器时,相关的连接对象也处于繁忙状态。在连接对象服务于数据读取器时,您可以执行的唯一操作是使用数据读取器 的 Close 方法。 OLE DB 和 SQL Server 数据读取器(ADODataReader 和 SQLDataReader 类)是非常小的 对象,用于避免创建不稳定内部对象。

基本上,如果客户端重复进行调用来读取数据(即,它调用循环中的 Item 属性 或 GetString 方法),则它始终通过相同对象获取传递的数据。为此,必须将通过数据读取器访问的数据视为只读,或者需要极其小心地 通过中间对象来处理这些数据。

IDataReader 接口

IDataReader 接口非常简单,并且提供了两种浏览级别:按结果集和按行。

可通过 IDataReader 浏览存储过程生成的各种结果,存储过程执行多个 SELECT 语句并返回其结果。数据读取器的默认位置是 第一个结果。使用 NextResult 向前移动,并使用HasMoreResults 检查剩下的任何结果。 当数据读取器内部指针位于一个特定结果上时,您可以浏览其行。使用 Read 可跳到下一行,而使用 Close 可终止读取。HasMoreRows 属性通知您当前位置和流结尾之间所有剩余的行。 正如您所看到的一样,此接口的方法仅涉及浏览模型。要进一步查看单个记录,您可以利用另一个接口,即 IDataRecord

IDataRecord 接口

您可以通过此接口访问该行中可用的单条信息。您经常使用的属性一定是 Item,它是数据读取器对象的索引器。每当您按名称访 问特定的字段时,您就会调用此属性:

Dim oDR As SQLDataReader = Nothing
oCMD.Execute(oDR)
While(oDR.Read)
	Response.Write(oDR["lastname"].ToString() + "
	")
End While
			

FieldCount 属性用于获取当前记录中的字段编号。

接口的其他部分由 Get XXX 方法的长列表组成,其中 XXX 代表类型。因此,您可以使用 GetStringGetInt32 等方法。例 如,GetInt32 返回指定字段的 32 位有符号整数值。可以按位置或名称来标识该字段。

oDR[0].ToInt32()
oDR["lastname"].ToInt32() 
			

您还可以通过类型为 Object 的常规值来读取数据。在这种情况下,请使用 GetValue

值得注意的一种特殊方法是 GetData 方法。基本上,如果字段递归指向实现 IDataReader 的 .NET 对象,则 GetData 返回此类对象。 SQLDataReader 对象还实现了ISQLDataReader, ISQLDataReader 是一个接口,它添加了一组 SQL Server 数据类型特定的其他 Get XXX 方法。例如,您可以使用 GetSQLGuid 来处理 SQLGuid 类型。

小结

我在本期专栏中首先比较了 ADO 和 ADO.NET 中的数据读取功能。ADO 中可以使用三种方法来读取记录,ADO.NET 本身只提供了其中的两种方法,并且是通过不同于 ADO 中的 对象实现的。 ADO.NET 并不实现(至少目前没有实现)服务器游标,但您可以通过 ADO.NET 以一种非常有效的方式从数据源中读取只进数据记录流。此外,它显著改进了对断开连接的数 据集的支持。

在本文中,我着重强调了 OLE DB 和 .NET 托管提供程序的可用数据读取器对象。在下一期中,我将详细地介绍 DataSet 和服务器 游标。

对话栏:我过去两年里一直都在做什么呢?

什么?OLE DB 不能在 Visual Basic 或 ASP 中使用?那我过去两年里一直都在做什么呢?您是否听说过连接字符串?难道只有 C++ 开发人员能够 使用 OLE DB 吗?更不要说我的所有 VB DLL 了!

如果“在 VB 或 ASP 中不能使用 OLE DB,那我过去两年里一直都在做什么呢?”呵呵,这又像是一个存在主义问题。 首先,上一期中引起质疑的一句话中包含一个额外的副词,在此处是至关重要的:直接。 我猜想您过去两年一直在通过 ADO 使用 OLE DB 使用者。特别地,您过去一直在使用连接字符串来标识目标 OLE DB 提供程序。非常好!在 Visual Basic_ 和 ASP 中,这 很容易做到。就算是现在,这也是一个比较好的方法。ADO 正是由于此原因而开发的。 实际上,ADO 是在 OLE DB 原始调用上生成的自动化包装类,其目的很明确,即允许从基于脚本的环境中使用 OLE DB,其中包括 Visual Basic 环境。 OLE DB 是一个基于 COM 的协议,它允许在使用者(客户端)和提供程序(服务器)之间建立数据驱动的通信。通过基于 COM,我 是说,为了直接使用它(没有中间过程),您应该:

1.创建表示提供程序的 COM 组件的实例。

2.查询提供数据源的接口。

3.在能够创建会话并返回相应接口的接口上调用方法。

4.使用会话接口创建一个命令并获取另一个(甚至是更适当的)接口。

5.使用该命令接口发出命令并获取 IRowset 接口,该接口是能使您获取 ADO 记录集的最接近的 OLE DB。

保留 IRowset 接口指针,此时,您与记录字段的内容仍有相当长的一段距离。您必须定义访问器元素(基本上是一个具有已知布 局的缓冲区),并找出一种使用实际数据填充它的方法。

这是实际的 OLE DB。而且,我们一致认为它是很难以使用的。 它非常复杂和麻烦,甚至在 C++(其中,查询接口以及管理指针和类型不受限制)中也是如此。 原则上,您可以考虑将所有此类直接调用机制移植到原始 Visual Basic 代码。实际上,您不会这样做。我认为不会有人真的要这样做。而且,您在 ASP 页中也无法完成此 任务。

当编程接口如此复杂时,采用包装类是唯一合理的方法。因此,包装具有扩展功能的层是完成此任务的正确方法,这种包装严格取决于这些层最初是针对哪种语言设计的。 因此,ADO 是为 Visual Basic 和 ASP 优化的 COM 自动化包装类,Visual Studio_ 6.0 中的 ATL 使用者类则为 C++ 开发人员提供了一种很好的解决办法。 使用 C++ 调用 OLE DB 提供程序公开的原始接口是获取和设置数据的捷径。实际上,我并不知道是否有可靠的统计数据说明 ADO 层对 OLE DB 数据访问的影响。从我个 人的经验看,本机访问 OLE DB 提供程序的 C++ 数据组件(带有或不带有 ATL 包装类)的速度通常约为使用 ADO 的 Visual Basic 组件的两倍。 另一方面,编写 C++ 代码的确更容易出错,并且您始终可通过使用更强大的硬件,最大限度地减少对性能的影响。(在现实生活中,有数不清的项目以这种方式解决此问 题。)

总而言之,C++ 是 OLE DB 的语言,但 ADO 可以按方便且强大的方式使 C++ 供 Visaul Basic 和 ASP 应用程序使用。

通过使用 .NET 托管提供程序,您最终可以使用最适合的语言来进行数据访问。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值