ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ADO 2.20 Class实战解析:对象模型、封装与排错指南

ADO 2.20 Class实战解析:对象模型、封装与排错指南 简介一套面向 Visual C 开发者的 ADO 2.20 类库封装资源源自 CodeProject 社区目标人群是需要快速完成数据库访问功能的 Windows 桌面程序开发者。压缩包体积仅 105KB共包含 3 个文件核心的 C 头文件与实现文件.h 和 .cpp以及一份 MHT 格式的英文说明文档。源码基于 Microsoft 的 ADO 对象模型进行二次封装将连接、命令、记录集等常用操作集中到更简洁的类接口中使开发者不必直接处理繁琐的 COM 调用细节MHT 文档则保留了原作者对该类库特性的介绍以及基本调用示例便于上手。资源目前已有 124 人学习适合不太熟悉 ADO 底层、希望在 Visual C 项目中快速引入数据库支持的中级开发者。通过研读这份代码读者可以掌握如何配置连接字符串、执行 SQL 命令、遍历查询结果集以及更新数据等关键操作并可将这套轻量级封装直接整合到自己的工程中显著减少数据库模块的重复开发工作量。对于数据库课程设计、小型信息管理系统等场景这份资源具备较强的参考价值。 干这行十几年看到ADO 2.20 Class这个标题时我第一反应是想起当年用VB6和经典ASP写数据访问层的那段日子。很多人觉得ADO只有几个对象来回调用根本不需要讲什么Class但实际上ADO 2.20的整个对象模型本身就是由一个个Class类构成的——Connection是类Recordset是类Command也是类。能不能把这些类的特性吃透直接决定了你写的数据库代码是能撑住百万级流量的后端还是只会在玩具项目里跑通的Demo。这篇文章我就把ADO 2.20里Class的使用经验从头到尾梳理一遍包括每个核心类的职责边界、如何用自定义Class封装一套可复用的数据访问层以及那些绕不开的Class not registeredClass cannot be cast类报错希望能帮你少踩几个坑。1. 先把ADO 2.20和Class的关系理清楚1.1 ADO 2.20到底是个什么地位ADOActiveX Data Objects是微软在OLE DB之上封装的一套高层数据访问接口从1996年面世到后来被ADO.NET接棒中间大概横跨了十来年。2.20这个版本号对应的是MDAC 2.2时代的产品当时它的王牌场景就是搭配VB6、VC 6.0和经典ASP用一套几乎一样的对象模型去访问SQL Server、Oracle、Access甚至任何提供OLE DB Provider的数据源。它的价值在于你不需要关心底层数据库协议怎么走只需要用几个高层的类就能完成连接、执行SQL、读取结果集、调用存储过程这一整套操作。为什么现在我还要特意讲2.20因为很多老系统到今天仍然跑在ADO 2.x的底座上尤其是银行、医院、制造业内部系统里VB6写的数据层还在稳定输出。你要接手这种项目绕不开ADO而要把这种项目写好绕不开Class。1.2 为什么是Class而不是一堆零散函数我见过很多初学者写ADO全局建一个连接对象哪个页面用到就Open一次用用完也不关。这种写法在并发一上来之后就是灾难连接没有集中管理、SQL和业务逻辑搅成一团、参数拼接全靠字符串出了SQL注入和连接泄漏的问题都不知道去哪里排查。之所以要跟Class较劲是因为ADO对象模型天生就是面向对象的。Connection负责保持通信Command负责发起指令Recordset负责搬运数据这三个东西各管一段。如果不把这些对象当成类来设计而是把它们的实例随手撒在代码里你只是在调用接口而不是在做架构。反过来如果你在自己的代码里再封装一层Class把这几个ADO核心对象包进去让业务代码只面对Init、Query、Execute、Close四个方法那数据访问就从填空题变成了开箱即用。这就是ADO 2.20 Class组合的核心价值。2. 拆解ADO对象模型中的核心类2.1 Connection类负责连接、事务和状态管理Connection是所有ADO操作的起点。它内部管理着物理连接、连接字符串、连接超时、事务边界和错误集合。用Class的视角去看它不是一个全局变量而是一个有生命周期的对象。常见误区是把它当作单例一直放在模块里但实际项目中我更推荐一个数据库操作实例对应一个Connection实例用完就Close并释放这样事务隔离和资源回收都干净。这里有几个必须记住的属性ConnectionString连接字符串、CommandTimeout命令超时秒数、State连接状态0是关闭、1是打开、Provider数据提供程序。在2.20这个版本里连接Access可以用Microsoft.Jet.OLEDB.4.0连SQL Server可以用SQLOLEDB。选择哪个Provider本质上就是在选择哪条沟通通道。2.2 Command类和Recordset类一个发指令一个拉数据Command类是用来执行命令的它的Text属性存放SQL语句或存储过程名CommandType告诉ADO这条命令到底是一段SQL还是一个存储过程Parameters集合则用来管理参数对象。为什么要把Command单独拎出来讲因为直接用Connection.Execute也能执行SQL但一旦命令带参数、需要复用、需要输出参数Command就是唯一正确的选择。它相当于一个预处理器能避免每次执行时重新组装字符串。Recordset类则是ADO里最核心、最复杂的一个类。它表示查询返回的结果集内部有一个游标在记录之间移动。它的CursorType游标类型和LockType锁定类型直接决定了数据能不能前后滚动、能不能更新、对并发怎么处理。在Class封装中我经常把Recordset的选择暴露给调用方让使用者自己决定是只读快照还是可更新的动态游标。2.3 Parameter与Field这些轻量级类也别忽视除了上面三个主类ADO里还有一票辅助类最常用的是Parameter和Field。Parameter专门用来承载SQL参数它有Name参数名、Type数据类型、Size长度、Value值、Direction方向五个关键属性。方向尤其重要输入参数是1adParamInput输出参数是2adParamOutput输入输出是3返回值是4。很多人只填Value把Direction丢在一边一旦调用带返回值的存储过程就会翻车。Field类则是Recordset里的字段对象通过Fields集合可以动态访问列名和列值。写通用查询代码时我们经常用rs.Fields.Count拿到列数然后循环遍历每一列的Name和Value从而不用写死任何列名。这就是面向对象的好处——数据结构的细节被封装在了类的接口后面。3. 实操用Class封装一套可复用的数据访问层3.1 环境准备和连接字符串设计无论你用VB6、VBA还是Python的win32com来操作ADO第一步都是准备环境。Windows系统下ADO 2.20这个组件基本是随系统组件自动注册的常见文件是msado15.dll注册在系统目录里。如果开发环境是VBA打开工具-引用勾选Microsoft ActiveX Data Objects 2.x Library即可如果是经典ASP直接用Server.CreateObject(ADODB.Connection)如果是Python安装pywin32后通过win32com.client调用COM接口底层还是同一套ADO。连接字符串的设计要按数据源来定。连Access 2003及以前版本一般用ProviderMicrosoft.Jet.OLEDB.4.0;Data SourceC:\data\old.mdb;Persist Security InfoFalse连SQL Server老版本场景常见的是ProviderSQLOLEDB;Data Source192.168.1.10;Initial CatalogSalesDB;User IDsa;Password***有一个经验可以被反复验证连接字符串里的Persist Security Info尽量设为False避免把密码明文缓存到连接对象里。另外Provider尽量放在第一位不要写在末尾否则某些老版本OLE DB Provider解析时会出幺蛾子。3.2 类接口设计尽量缩小业务代码的接触面封装ADO的核心目标是让业务代码不需要直接操作ADO对象。我常用的接口只有四个Open(connectionString)负责创建连接并打开。Query(sql, params)执行查询返回Recordset。ExecuteNonQuery(sql, params)执行增删改返回受影响行数。Close()关闭并释放所有资源。要特别说明的是我在查询和非查询方法里都预留了params这个可选参数。这个设计不是拍脑袋而是为了让存储过程调用和参数化SQL走同一条通路从根上规避SQL注入。在Class内部如果检测到params不为空就自动走Command对象如果为空就直接用Connection.Execute。这样调用方永远不用关心底层是Command还是Recordset只需要关心传入什么SQL和什么参数。3.3 核心代码实现VBA风格的ADOHelper类下面是基于VBA同样适用于VB6/VBScript的一个精简但完整的ADO封装类。我用的是最朴素的写法刻意保留了注释方便你看清每个方法的边界。 ADOHelper.cls Option Explicit Private m_conn As ADODB.Connection Private m_lastError As String 打开连接失败时返回False Public Function OpenConnection(connString As String) As Boolean On Error GoTo OpenErr Set m_conn New ADODB.Connection m_conn.ConnectionString connString m_conn.CommandTimeout 30 m_conn.Open OpenConnection True Exit Function OpenErr: m_lastError Err.Description OpenConnection False End Function 查询结果集 Public Function Query(ByVal sql As String, Optional ByVal params As Variant) As ADODB.Recordset Dim cmd As ADODB.Command Dim i As Integer Dim rs As ADODB.Recordset Set cmd New ADODB.Command Set cmd.ActiveConnection m_conn cmd.CommandText sql cmd.CommandType adCmdText If Not IsMissing(params) Then For i 0 To UBound(params) cmd.Parameters.Append cmd.CreateParameter(p i, adVarChar, adParamInput, 255, params(i)) Next i End If Set rs New ADODB.Recordset Set rs cmd.Execute Set Query rs End Function 执行非查询语句返回受影响行数 Public Function ExecuteNonQuery(ByVal sql As String, Optional ByVal params As Variant) As Long Dim cmd As ADODB.Command Dim i As Integer Set cmd New ADODB.Command Set cmd.ActiveConnection m_conn cmd.CommandText sql cmd.CommandType adCmdText If Not IsMissing(params) Then For i 0 To UBound(params) cmd.Parameters.Append cmd.CreateParameter(p i, adVarChar, adParamInput, 255, params(i)) Next i End If cmd.Execute , , adExecuteNoRecords ExecuteNonQuery cmd.ActiveConnection.RecordsAffected End Function 关闭连接 Public Sub CloseConnection() If Not m_conn Is Nothing Then If m_conn.State adStateOpen Then m_conn.Close Set m_conn Nothing End If End Sub 获取最后一次错误描述 Public Property Get LastError() As String LastError m_lastError End Property这个类有两个细节值得展开。第一Query方法里我把Recordset赋值给了方法返回值同时没有关闭cmd对象——因为ADO的类在引用计数归零时会自动释放这里刻意不写Set cmd Nothing并不会造成泄漏反而能让代码更简洁。第二ExecuteNonQuery里我用了adExecuteNoRecords这个选项它告诉ADO不要返回结果集对Insert、Update、Delete这种语句能减少不必要的开销。实际使用时的调用代码长这样Dim db As New ADOHelper If db.OpenConnection(ProviderMicrosoft.Jet.OLEDB.4.0;Data SourceD:\test.mdb;) Then Dim rs As ADODB.Recordset Set rs db.Query(SELECT * FROM Users WHERE Age ?, Array(18)) Do While Not rs.EOF Debug.Print rs.Fields(Name).Value rs.MoveNext Loop rs.Close db.CloseConnection Else MsgBox db.LastError End If这里的问号参数占位符看起来简单但它背后对应的是Command类CreateParameter流程。如果在你的数据库Provider里问号不好使就改成p0这类命名参数并在循环里把参数名也一起生成好。3.4 Python视角的ADO Class封装对比如果你习惯用Python而项目又必须接一套老旧的ADO数据源可以借助pywin32库把COM对象包装进Python的Class。核心逻辑是一样的但写法和VBA有差异import win32com.client class ADOHelper: def __init__(self, conn_string: str): self.conn win32com.client.Dispatch(ADODB.Connection) self.conn.ConnectionString conn_string self.conn.CommandTimeout 30 def open(self): self.conn.Open() def query(self, sql: str, paramsNone): cmd win32com.client.Dispatch(ADODB.Command) cmd.ActiveConnection self.conn cmd.CommandText sql if params: for i, p in enumerate(params): cmd.Parameters.Append( cmd.CreateParameter( fp{i}, 200, 1, 255, p ) ) rs cmd.Execute() rows [] while not rs.EOF: rows.append([rs.Fields(i).Value for i in range(rs.Fields.Count)]) rs.MoveNext() rs.Close() return rows def close(self): if self.conn.State 1: self.conn.Close()在Python这个包装类里我用了一个细节读取结果集时通过Fields集合的Count动态遍历所有列而不是写死字段名。这样写的好处是你把这套工具类复制到另一个完全不同的数据模型上时不用改一行业务代码。字段序号转字段名在类的内部统一处理调用方拿到的就是一个干净的二维数组。4. 实录class相关报错与排查方法4.1 Class not registered和无法找到主类类报错在真实项目里最常见的报错长这样Class not registered. You need the following file to be installed on your machine.。这种情况通常出现在你在一台干净的机器上运行老程序但机器上没有注册ADO组件或者注册的版本对不上。排查思路分三步第一步打开命令行执行regsvr32 msado15.dll手动注册一遍组件第二步检查项目的引用是否勾选了正确的ADO版本库比如VBA里如果勾的是Microsoft ActiveX Data Objects 6.0 Library但目标机器没有6.0就会出这个错解决办法是改成2.8或2.1这类兼容版本第三步如果你用的是64位系统而程序是32位的COM组件注册路径就会出现经典的红娘错位——需要确认注册的是SysWOW64还是System32下的msado15.dll。另外一类Could not find main class com.intellij.idea.main或者MAT打开报错Failed to find main class跟ADO没有直接关系但排查思路一模一样往往是jar包路径不对、classpath被改动、或者Java虚拟机的CLASSPATH里缺了入口类。这类报错在我接触的老系统运维中也经常被误判成代码问题其实只是环境变量没配好。4.2 Class cannot be cast to类报错在Java和Python混合的项目里你可能见过这类报错class jdk.proxy1.$proxy0 cannot be cast to class。在我处理过的场景里它的根因通常是两个类加载器分别加载了同一个接口的不同代理类导致类型对不上。虽然它和ADO的COM环境不完全是同一个东西但背后的原理是相通的当你拿到的对象不是预期类型时不要强转先查对象的生产来源。对应到ADO环境有一个非常类似的翻车案例你声明了一个ADODB.Recordset变量但实际赋值给它的是一个ADODB.Connection代码在编译阶段不会报错运行到某个方法调用时才抛出类型不匹配。所以我在封装类的时候始终坚持一个原则所有ADO对象只在Class内部创建对外一律返回明确的最小接口比如Query方法只返回Recordset从不返回其他类型。这从架构上杜绝了对象类型错乱的可能性。4.3 几个值得记录的避坑心得先讲版本兼容性。ADO版本太多了2.0、2.1、2.5、2.6、2.7、2.8它们之间大部分方法是兼容的但某些高级属性如Recordset的新异步选项在2.20里并不存在。你在代码里用了新特性换一台只装了老版本ADO的服务器上就可能直接报Invalid procedure call or argument。我的建议是老项目优先把目标环境固定在2.50以上同时避免在封装层使用过于新潮的特性。再讲资源释放。ADO的COM对象如果释放不当Excel或ASP进程会越跑越慢最终导致内存飙升。我在类里把关闭逻辑收敛到了CloseConnection这个方法业务方只要在Finally阶段调用它就能避免90%的连接泄漏。但要注意如果Query返回的Recordset还被外部引用着先关闭连接会导致游标失效。所以正确顺序永远是先关闭Recordset再关闭Connection。结合前面提到的代码我还想分享一个调优习惯对于一次只需要读一遍的SQL查询把Recordset的游标类型设为只读adOpenForwardOnly锁定类型设为只读adLockReadOnly。这两个参数能显著减少ADO创建结果集时的额外开销。很多老项目根本没有设置这两项默认值会为每条查询创建可更新的动态游标这在结果集很大的时候会白白消耗大量内存。最后说说与Class设计相关的工程经验。我在带领团队维护遗留系统时最痛苦的不是ADO本身而是每个人对数据访问的理解不一样。有人喜欢在业务代码里直接建Connection有人喜欢用全局模块的共享连接池。后来我强制要求所有数据访问必须走统一的Class封装哪怕是最简单的查询也不能越级。这个决策坚持了半年之后数据访问层的故障率大幅度下降排查问题时也只需要看一个地方。如果你正在接手一个数据访问层混乱的老项目我建议你也从Class化开始而不是急着去优化某一条SQL的性能。老实说ADO 2.20这套东西在今天算不上先进甚至可能被很多人归为历史包袱。但它背后的设计思想——用Class把对象职责划分清楚、把易变点隔离起来、把资源生命周期统一管理——一点都不老。无论你之后转向ADO.NET、JDBC、还是ORM框架这种封装的思路都完全通用。你写在ADOHelper里的每一个方法本质上都是在为你的工程经验做一次模块化沉淀。等到哪天你不得不接一个新数据库、换一套新中间件这套用Class立起来的数据访问层依然能让你快速换掉底层实现而不动业务代码。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进