简介:一套面向 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 registered""Class cannot be cast"类报错,希望能帮你少踩几个坑。
1. 先把ADO 2.20和Class的关系理清楚
1.1 ADO 2.20到底是个什么地位
ADO(ActiveX 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(方向)五个关键属性。方向尤其重要:输入参数是1(adParamInput),输出参数是2(adParamOutput),输入输出是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及以前版本,一般用:
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\data\old.mdb;Persist Security Info=False连SQL Server(老版本场景),常见的是:
Provider=SQLOLEDB;Data Source=192.168.1.10;Initial Catalog=SalesDB;User ID=sa;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("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\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, params=None): 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( f"p{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立起来的数据访问层,依然能让你快速换掉底层实现而不动业务代码。
本文还有配套的精品资源,点击获取