ADO在.NET应用程序中挥洒自如 博客分类: Database General .netASP.net应用服务器编程ASP
ADO 在 .NET 应用程序中挥洒自如
软件地质学家声称这种岩石起源于后 Internet 时代,即在第一次 ODBC 冰河期后出现。在过去的数年时间里建立的所有 DNA 系统中,都多次发现了这种矿石代码 — 在这段时间里,无疑存在地质学时代的软件雏形。
了解 ADO 岩石的形成过程有助于获得有关上一个软件时代中的周围环境和业务逻辑的有用信息。推断过去并了解其促成因素始终是一种判断如何规划和面向未来的有效方法。
这种基本的哲学原则在翻译为软件术语后,就成了一条人们耳熟能详的警示:“做好管理您的遗传代码的准备。”(我们所有人都有某些种类的遗传代码,因此,至少在没有翻箱倒柜地进行仔细查找之前不要断言您没有遗传代码。)
从 .NET 应用程序的观点来看,所有 ADO 代码都是遗传代码。事实上,要在 .NET 中完成数据处理,您应该使用新的 ADO.NET 类。如果您剪切和粘贴使用 ADO 对象的现有 Visual Basic_ 或 ASP 应用程序,并将它保存为 .NET 等效应用程序,则一定会出现许多错误,对此,您一定要有心理准备。
简而言之,您可以将 ADO 类导入到任何类别的 .NET 应用程序(Windows 窗体、Web 窗体或 Web 服务)或者将其替换为 ADO.NET。问题难就难在,人们将 ADO.NET 看成是基于 Web 的下一级 ADO。然而,它在功能上并不等同于 ADO,但也不是与 ADO 完全无关。
正如您所看到的一样,这会产生许多重要的设计问题,其中最重要的无疑是在 .NET 应用程序中如何集成 ADO。第二点(但绝非不重要)是找出 ADO.NET 和 ADO 在功能上的差异,以及因而为在两个框架中获得相同结果而应采取的措施。
本期将设法详细解答这些问题。换句话说,此处,我最大目的是描述一些基本的规则,以预测在您以后的一些项目中可能出现的代码地震或功能冰河。
在穿越 COM 河流的过程中架起了一座 Interop 的桥梁(此外,需要修改样式)
对于所有 .NET 应用程序,ADO 就是一个外部组件,即各种组件模型的子模型。对于所有 .NET 应用程序,调用 ADO 对象与调用任何其他 COM(+) 组件绝无两样。所有 .NET 应用程序必须执行一些非常规的操作来调用 COM(+) 组件。在 nutshell 中,ADO 在 .NET 领域是一个不折不扣的门外汉。
另一方面,.NET 框架只不过是在不断发展的组件结构中向前迈出的自然而然的一步,这种结构是 COM 在数年前创立的。尽管 COM 和 .NET 有很多共同之处(包括组件重复使用和语言中立性),但两者仍是差别很大的实体。要从其中一个实体调用另一个实体,需要搭建特殊的桥梁来保证兼容性。
COM Interop 模块提供了从 .NET 应用程序访问现有 COM 组件所需的中间代码层。它作为本地代理,并负责将 COM 组件公开为本机 .NET 类。更为重要的是,它执行此操作时不需要修改原始组件。
为将现有的 COM 组件加入到 .NET 托管应用程序中,您可以通过系统提供的特殊实用程序(称为 TLBIMP)导入组件的类型库。它为 .NET 应用程序提供一个与 COM 对象具有相同编程接口的新框架类。完成此操作后,调用 COM 对象就像调用本机 .NET 类一样。
如何导入 ADO
TLBIMP.EXE 是 .NET 框架提供的一个免费系统实用程序。它生成一个程序集,其中包含基于指定 COM 类型库的常规 .NET 元数据。(存在类型库是完成此操作的必要条件。)
因此,要将 ADO 对象类型导入到 .NET,您需要按如下方式操作:
tlbimp.exe msado15.dll
msado15.dll 中包含对象的整个 ADO 层次结构,该文件通常安装在 c:\program files\common files\system\ado 文件夹中。在这种情况下,TLBIMP 在当前文件夹中创建一个名为 adodb.dll 的文件。除非您指定一个输出文件名,否则该实用程序将使用库的名称。您可以在命令行中使用 /out:name 开关设置输出名称。
如果您使用 Visual Studio .NET 来生成 .NET 应用程序,您甚至不需要了解 TLBIMP。在这种情况下,在解决方案资源管理器中右键单击“引用”节点,并从提供的已注册的 COM 组件列表中选取 ADO 库即可。
在完成此操作后,就可以在 .NET 代码中将 ADO 对象作为本机类使用。对于从 SQL Server 表提取某些记录并通过列表视图对它们进行显示的典型 Visual Basic 应用程序,现在可以使用 C# 进行重写,如下所示:
String conn = "PROVIDER=SQLOLEDB;INITIAL CATALOG=Northwind;" + "SERVER=localhost;UID=sa;PWD=;"; String cmd = "SELECT firstname, lastname FROM Employees"; ADODB.Recordset adoRS = new ADODB.Recordset(); adoRS.Open(cmd, conn, ADODB.CursorTypeEnum.adOpenForwardOnly, ADODB.LockTypeEnum.adLockReadOnly, 0); while (!adoRS.EOF) { listView1.ListItems.Add(adoRS.Fields["lastname"].Value + ", " + adoRS.Fields["firstname"].Value); adoRS.MoveNext(); }
此处的关键在于,在代码需要新的 ADODB.Recordset 对象实例时,会发生什么情况。您在下面看到的 Windows 窗体应用程序是一个 .NET 客户端,表面上看来该客户端是在创建一个 ADO 记录集 COM 对象实例。然而,在内部所发生的情况却略有不同。
图 1. 使用 ADO 提取数据的 Windows 窗体应用程序
客户端仍然与一个常规 .NET 类通信,该类称为运行库可调用包装类 (RCW),其工作方式就像原始 Recordset 对象一样。在创建 RCW 类时,它立即实例化目标组件。然后,RCW 类的工作方式就像未托管组件的某种代理一样。
未托管对象与其 .NET 版本类具有一对一的关系。不同 COM 组件实例是通过不同 RCW 类管理的,但没有使用新的 RCW 对相同实例进行额外的引用。
RCW 基础结构负责在托管代码和非托管代码之间封送方法参数和返回值。当然,仅当 COM 和 .NET 中的数据表示形式不同时,才会进行这种类型的转换。例如,BSTR 变量必须变为字符串变量,反之亦然。
COM 和 .NET 在很多方面是不同的,这些方面包括异常处理、内存管理、函数标识和线程处理等等。RCW 层必须以某种方式隐藏所有这些差异,以使连接的每一端(RCW 类和 COM 对象)看到它知道如何使用的编程接口和环境。
COM 客户端应该直接控制它们所实例化的对象的生命周期。相反,.NET 客户端知道系统垃圾回收器将随后清理内存。RCW 类(TLBIMP 在所生成的程序集中对其进行硬编码)是从知道如何处理此差异的基类继承的。
类似地,.NET 应用程序应该以线程安全的方式公开共享资源。而 COM 客户端应该知道服务器组件支持的线程模型。需要重申的是,RCW 类负责在正确类型的单元内实例化目标 COM 对象。
COM 对象通过 HRESULT 返回代码返回成功或失败,而 .NET 客户端从引发的异常了解到其调用出现了错误。RCW 则将其遇到的任何失败的 HRESULT 值都转换为 .NET 异常。(因此,应该避免使用通过 HRESULT 返回提示性信息内容的 COM 对象。)
现在我们应该清楚以下事实:从 .NET 客户端调用 ADO 对象是可行的,并且并不是特别费事。但是,每次从 .NET 上下文中调用 COM 服务器对象时,将在您的应用程序和实际对象之间不知不觉地插入大量代码。您与看不见的代理进行通信,该代理又进而询问 COM 对象,而使您看不到 COM 和 .NET 之间的结构差异。
代理给您造成快速而方便地进行通信的错觉。实际上,每次您调用它时,都要在波涛汹涌的水上搭建一座桥梁。计算此过程的实际工作量完全在于您自己,并且取决于您的项目。
这种系统开销只影响通信信道,可通过此信道在 .NET 客户端和 COM 组件之间传递请求和响应。如果 COM 组件又调用其他 COM 对象(例如,使用 ADO 或 ATL COM 使用者调用 OLE DB 提供程序的 Visual Basic 数据访问组件),则不需要进一步进行处理。
COM Interop 层的开销是由于在 .NET 岛和 COM 陆地之间搭建桥梁造成的。因此,您只支付一次全价。访问 ADO 的 .NET 客户端执行相同的操作,无论它是直接调用 ADO 对象模型,还是向下调用不同的数据访问组件。
在 .NET 中使用记录集
如果您使用 ADO,则最终要用到 Recordset 对象。但是,ADO.NET 中没有此类对象。这本身并不是什么问题,只要您知道正在做什么就行了,即,坚持使用一种过时的编程模型。
随着时间的推移,Recordset 对象发生了很大变化。最初,它只是 COM 版本的 ODBC resultset 而已,并增加了一些非常有用的功能(例如,书签、筛选和排序)。从 ADO 2.6 起,Recordset 支持流、XML 持久性、批处理更新、数据断开、手动构造和数据构形等等。
尽管提供了这么多功能,Recordset 并未提供处理多个数据表的功能,或者使用存储过程和自定义命令来批处理更新数据源。此外,Recordset 是一个 COM 对象,它很难与其他在非 Windows 平台上运行的模块进行交换。再者,如果您要将记录集与其他在基于 Windows 的平台上运行的不同模块进行交换,您必须在 COM 封送和类型转换层完成大量的工作。
为提高整个数据处理子系统的互操作性和可伸缩性,.NET 推出了一种以数据为中心的新数据模型,它与 ADO 和 OLE DB 以数据库为中心的标准模型相反。从以下三个重要变化就可以清楚地看到这一点:
• |
将 ADO Recordset 的功能分成不同的且更加简单的对象。 |
• |
引入了通用的数据容器对象,即DataSet。 |
• |
在 DataSet 对象中嵌入了 XML 数据表示形式,从基本上讲,就是合并了两种编程接口(关系型和分层型)。 |
套用爱因斯坦的话说,.NET 试图使数据访问尽可能简单(和轻便),但同时又具有足够高的可靠性和强大的功能以满足最苛刻的要求。与 ADO 相比,ADO.NET 的对象模型设计是一个重大的转变。ADO.NET 并不是像 Recordset 那样庞大且相当单一的对象,它是一组更简单、更加专用的对象。“专用与多功能性”就是软件在日常生活中遇到的另一个双重矛盾。
可以在 DataTable 对象中找到 .NET 版本的 Recordset 对象。即使 DataTable 仅实现了 Recordset 的基本功能(必须处理数据读取和写入的功能),这种说法也站得住脚。
在 .NET 中,目前的 ADO 代码使用的 Recordset 所遵循的原则不再有效。这意味着,很难将 Recordset 与典型 .NET 应用程序的其他元素集成在一起。例如,您不能使用 Recordset 填充 ASP.NET 或 Windows 窗体数据网格。
而您能做的就是将 Recordset 转换为 .NET 数据容器类。以下代码片段使用先前生成的 Recordset,并建立等效的 DataTable:
DataTable dt = new DataTable("AdaptedFromRecordset"); dt.Columns.Add ("EmployeeName", System.Type.GetType("System.String")); while (!adoRS.EOF) { DataRow dr; dr = dt.NewRow(); dr["EmployeeName"] = adoRS.Fields["lastname"].Value + ", " + adoRS.Fields["firstname"].Value; dt.Rows.Add (dr); adoRS.MoveNext(); } dataGrid1.DataSource = dt.DefaultView;
正如以下函数所显示的那样,可以将此代码方便地推广到任何记录集:
DataTable RecordsetToDataTable(ADODB.Recordset adoRS, String strTable) { // Assumes the recordset is open and moves on the first record adoRS.MoveFirst(); DataTable dt = new DataTable(strTable); // Loops through the Recordset's fields for(int i=0; i < adoRS.Fields.Count; i++) { String strColName = adoRS.Fields[i].Name; Type t = adoRS.Fields[i].Value.GetType(); dt.Columns.Add (strColName, t); } // Loops through records and columns while (!adoRS.EOF) { DataRow dr = dt.NewRow(); for(int i=0; i < adoRS.Fields.Count; i++) dr[i] = adoRS.Fields[i].Value; dt.Rows.Add (dr); adoRS.MoveNext(); } // Leaves the recordset on the last record return dt; }
该代码首先滚动 Recordset 字段集合,然后将列添加到新创建的 DataTable 对象中。新列保留了 ADO 字段的名称,并且系统给其指定了等效的 .NET 类型。
Type t = adoRS.Fields[i].Value.GetType();
注意,RCW 引擎对 RecordsetField 对象的原始接口进行了一定的修改。它添加了返回 .NET System.Type 对象的 GetType 方法。ADO Field 对象中没有此类方法。
下一步,该函数遍历记录并填充 DataTable 行集合。
您可以按以下方式使用此 DataTable 对象:
DataTable dt = RecordsetToDataTable(adoRS, "AdaptedFromRecordset"); dataGrid1.DataSource = dt.DefaultView;
如果您打算搭建一座桥梁,使现有 ADO 代码无缝地流入您的 .NET 应用程序中,则做好准备将这座桥梁向前延伸,从 ADO 对象一直连接到类似的 ADO.NET 对象。
使用函数来隐藏将 ADO 和 ADO.NET 对象无缝地集成在一起所需的额外代码简直就是面向对象的工作。一种更为完善的解决方案是,设置一种从 DataTable 继承并添加了两种新的构造函数的 AdoDataTable 类。此外:
public DataTable(); public DataTable(String);
它还具有以下优点:
public DataTable(ADODB.Recordset); public DataTable(ADODB.Recordset, String);
前者将使用默认的表名称,而后者将使用指定的名称。该类的源代码类似于:
using System; using System.Data; public class AdoDataTable : System.Data.DataTable { public AdoDataTable(ADODB.Recordset adoRS) { RecordsetToDataTable(adoRS, "MyNewTable"); } public AdoDataTable(ADODB.Recordset adoRS, String strTableName) { RecordsetToDataTable(adoRS, strTableName); } private void RecordsetToDataTable(ADODB.Recordset adoRS, String strTable) { // Assumes the recordset is open adoRS.MoveFirst(); // Loops through the Recordset's fields for(int i=0; i < adoRS.Fields.Count; i++) { String strColName = adoRS.Fields[i].Name; Type t = adoRS.Fields[i].Value.GetType(); Columns.Add (strColName, t); } // Loops through the Recordset's records to populate the DataTable while (!adoRS.EOF) { DataRow dr; dr = NewRow(); for(int i=0; i < adoRS.Fields.Count; i++) dr[i] = adoRS.Fields[i].Value; Rows.Add (dr); adoRS.MoveNext(); } // Leaves the recordset on the last record } }
.NET 应用程序只需在项目中引用此类,并使用以下极其简单的代码便可获取与以前相同的结果:
AdoDataTable adt = new AdoDataTable(adoRS, "AdaptedFromRecordset"); dataGrid1.DataSource = adt.DefaultView;
在编写该类时,要在每种方法之前添加此特殊的注释:
/// <summary> /// Comment here /// </summary>
Visual Studio .NET 将使用此文本来提供免费的 IntelliSense 支持。
一般来说,Recordset 可帮助您逐步迁移到 .NET,但您越早迁移到 ADO.NET,您的迁移过程的效果就会越好。正如我在上一期专栏中所说的一样,一旦您选择了 .NET,您便没有两全的办法。要么保持系统不变,要么从头开始重新考虑该系统,并制订中期或长期的迁移计划。
服务器游标
从 Beta 1 起,ADO.NET 不再直接支持以下数据库编程功能:服务器游标。如果您需要在代码中使用服务器游标,则必须借助于 ADO 功能来实现。
ADO.NET 是设计用来满足大多数要求的。此原则要求小组将设计重点放在中断连接的数据访问和客户端游标上,而不是放在可伸缩性差的(但有时是必需的)服务器端游标上。
服务器游标始终需要打开的连接,它代表了源于过去客户机/服务器计算的设计方法。虽然现在仍存在许多只能使用服务器游标的应用程序,但基于 Web 的系统根本不需要它们。
因此,如果当前代码使用服务器游标,您应该怎么办呢?要做的第一件事就是,慎重考虑是否选择放弃动态的服务器端游标而使用静态的客户端游标,静态的客户端游标是 ADO.NET 编程接口中首选和推荐的游标。
如果您不能更改代码以使用客户端游标,请继续使用 ADO。
它是否会挥洒自如?
在 .NET 应用程序中使用 ADO 并不是非常困难的事。但是,这是一种短期方法,因为它破坏了 .NET 类的完整性。它使您(勇敢的 .NET 开发人员)*使用非常规的编程方法,更为重要的是,在将数据与编程接口的其他部分合并时,它可能会引发某些问题。要一劳永逸地解决这一问题,您现在可以做一些重新编写工作以期将来提供长期的 ADO.NET 收益。正如任何一般规则一样,总是会有一些例外情况。在这种情况下,服务器游标是最显著的一个。.NET 中的 ADO 代码可能像石头一样坚硬,但从设计的角度看,维护和代码过时也可能会使您的代码僵化死板。
Developer’s Network Journal 2001 年 2 月刊中刊登了另一篇很好的 ADO.NET 文章。要阅读这篇文章,请单击以下链接:http://www.dnjonline.com/articles/essentials/iss22_essentials.html。
对话栏:您是否(真的)准备好进行迁移?
Dino,您好!感谢您提供了非常精彩的 .NET 迁移文章。但是,如果我即将开始一个新项目,我不知道是否来得及使用 .NET。
要牢记的四点是:
• |
我将向我在 Microsoft 的联系人了解有关发行时间表的最新新闻。我并不在意像 Visual Studio .NET 之类的开发工具的可用性问题。我关心更多的是平台二元性问题和好的文档。在发行前三个月内进行准备就可以了,而不必在设计级别采取太多的预防措施。否则,我采取 50% 对 50% 的策略,因为您做两手准备总不会有什么损失。实际上,我在在适当的时间进行迁移中介绍了如何进行规划。 |
• |
项目性质也是一个考虑事项。ASP.NET 与 Beta 2 很接近,我认为它在此时已非常可靠并且设计非常完善。我预计 API 不会发生很大的变化,而只是一些较小的更改和增加新的功能而已。当然,我预计在内存管理和总体性能方面还会有所提高。我认为 ADO.NET 也不会有什么变化。这包括简单的读取功能和更新。有关更复杂的需要,我只好等待 Beta 2 了。顺便提一下,预计 Beta 2 是RTM 之前的最终测试阶段。这应该给您提供了我做事情的思路:一半 ASP,一半 ASP.NET。 |
• |
另一个考虑事项是最终期限 — 越循序渐进,效果越好。如果要我给上述问题做出一个二元性的解答(顺便说一下,这根本无关紧要)的话,我要说的是:“我还没有准备好使用 100% 纯粹、真正的 .NET 系统。”我刚刚将我的 Intranet 作为 ASP.NET 应用程序进行了重新编写,但与真实的系统相比,它看起来有点过于简单了。但是,您不费力气或方便地进行编码就可获取的功能达到的水平是非常令人吃惊的。如果将它与现实情况结合起来,那么,它具有的潜在优点无疑是会非常诱人和令人振奋的。我敢打赌,现在就迁移到 .NET 可赢得非常大的先机。我喜欢打赌,但我会确保了解尽可能多的游戏规则。 |
• |
我的所有客户都对 .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。