ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

ADO2.20 Class 实战:连接、命令与记录集核心用法与避坑指南

ADO2.20 Class 实战:连接、命令与记录集核心用法与避坑指南

简介:ADO2.20 Class 是一套面向 Visual C++ 开发者的 ActiveX Data Objects 类库,最初由国外开发者在 CodeProject 发布,旨在简化 VC 程序对 SQL Server、Oracle、Access 等数据源的访问,让开发者无需深入底层 ADO 接口即可完成数据库操作。资源包共 3 个文件,包含 1 个 cpp 实现文件、1 个 h 头文件以及 1 个 mht 说明文档,压缩包约 105KB,体积轻巧便于集成到现有工程。其中头文件提供类定义与接口声明,cpp 文件封装了 Connection、Command、Recordset 等核心对象的常用操作,mht 文档则保留了原版使用说明与示例参考。目前已有 126 人学习关注,适合不熟悉 ADO 或需要快速为 VC 项目添加数据库访问能力的开发者,通过阅读源码即可理解连接管理、查询执行与结果集遍历的封装思路,并直接复用到实际工程中。

1. ADO2.20 Class:从连接字符串到记录集关闭的完整链路

如果你维护过 VB6 或早期 ASP 项目,大概率见过ADODB.Connection、ADODB.Recordset这类写法。ADO2.20 是 Windows 平台上 MDAC 组件里一个非常经典的版本,很多老系统的数据访问层至今还跑在它上面。问题在于,这套东西的文档大多停留在“能跑就行”的层面,真正遇到连接池耗尽、游标类型选错导致内存暴涨、Recordset没关干净引发句柄泄漏时,很多人只能靠猜。这篇笔记围绕 ADO2.20 的 Class 使用展开,把连接对象、命令对象、记录集对象三者的关系拆开讲,给出可复现的代码和参数配置,适合正在维护老系统、或者需要把老数据访问层迁移到新环境的工程师。核心不是背 API,而是搞清楚每个对象在什么场景下该用什么游标、什么锁、什么超时。

2. ADO2.20 三个核心 Class 的职责边界与选型

2.1 Connection、Command、Recordset 到底谁管什么

很多人写 ADO 的习惯是:一个Connection打开,然后所有查询都靠Connection.Execute返回一个Recordset,从头用到尾。这种写法在小数据量下没问题,但一旦涉及参数化查询、存储过程、批量更新,就会暴露出职责混乱的问题。

Connection的职责只有三件事:建立物理连接、管理事务边界、控制连接超时。它不应该承担 SQL 拼接和结果集遍历的工作。Command的职责是封装一条可带参数的指令,它可以绑定到某个Connection上执行,也可以独立定义后再指定活动连接。Recordset则是结果集的载体,它的行为完全由CursorType和LockType两个属性决定。

我一般会这样划分:凡是需要重复执行、带用户输入的 SQL,一律走Command加Parameters;只有一次性、无参数的简单查询才用Connection.Execute图省事。这个边界一旦立住,后面排查注入风险和性能问题会轻松很多。

2.2 游标类型和锁类型的组合矩阵

CursorType有四个常用取值,LockType也有四个,组合起来不是随便选的。下面这张表是我在实际项目里验证过的组合建议:

CursorType常量值适用场景推荐 LockType
adOpenForwardOnly0只读遍历、导出adLockReadOnly
adOpenStatic3需要行数统计的只读列表adLockReadOnly
adOpenKeyset1需要看到他人修改的交互界面adLockOptimistic
adOpenDynamic2极少用,开销大adLockOptimistic

默认的adOpenForwardOnly加adLockReadOnly是性能最好的组合,因为它不维护游标位置,只做单向流式读取。一旦你把CursorType改成adOpenStatic,ADO 会在客户端或服务端物化整个结果集,数据量大时内存直接翻倍。adOpenDynamic更是重灾区,它会持续跟踪其他会话的增删改,网络往返次数成倍增加。

2.3 用 Command 对象做参数化查询的最小可跑代码

下面这段 VBScript 风格的代码,演示了从连接建立到参数绑定的完整流程。注意Parameters.Append的顺序必须和 SQL 里?占位符的顺序一致,这是 ADO 的一个硬性约束。

Dim conn, cmd, rs Set conn = CreateObject("ADODB.Connection") conn.ConnectionString = "Provider=SQLOLEDB;Data Source=127.0.0.1;Initial Catalog=TestDB;User ID=sa;Password=***;" conn.ConnectionTimeout = 10 conn.CommandTimeout = 30 conn.Open Set cmd = CreateObject("ADODB.Command") Set cmd.ActiveConnection = conn cmd.CommandType = 1 ' adCmdText cmd.CommandText = "SELECT OrderId, Amount FROM Orders WHERE CustomerId = ? AND Status = ?" ' 参数顺序必须与 SQL 中 ? 出现顺序一致 cmd.Parameters.Append cmd.CreateParameter("CustomerId", 3, 1, 4, 1001) ' adInteger, adParamInput cmd.Parameters.Append cmd.CreateParameter("Status", 200, 1, 20, "PAID") ' adVarChar Set rs = cmd.Execute Do While Not rs.EOF WScript.Echo rs.Fields("OrderId").Value & " - " & rs.Fields("Amount").Value rs.MoveNext Loop rs.Close Set rs = Nothing Set cmd = Nothing conn.Close Set conn = Nothing

逻辑说明:CreateParameter的第二个参数是数据类型,3代表adInteger,200代表adVarChar;第三个参数1是adParamInput,表示输入参数;第四个参数是长度,整数类型给 4 即可,字符串类型必须给足长度,否则会被截断。参数说明里最容易翻车的是字符串长度,很多人写255结果实际字段只有20,虽然不报错但会浪费内存。

2.4 连接字符串里的三个必调参数

连接字符串不是越长越好,但下面三个参数我建议显式写上:

  • ConnectionTimeout:默认 15 秒,内网可以降到 5 秒,避免网络抖动时线程被长时间挂住。
  • CommandTimeout:默认 30 秒,复杂报表查询可以调到 120 秒,但不要设 0(无限等待)。
  • OLE DB Services:如果不需要连接池和自动事务,可以设为-4关闭部分服务,减少资源占用。

提示:连接字符串里的密码如果明文写在代码里,任何能读到文件的人都能拿到。老系统迁移时优先把认证方式换成集成认证,或者至少把密码放到受控的配置文件里。

3. 把 ADO2.20 跑起来:环境准备与最小验证

3.1 确认 MDAC 版本和 ADO 类型库注册状态

ADO2.20 对应的类型库文件通常是msado20.tlb,注册在系统目录下。在动手写代码之前,先用一条命令确认它是否可用:

reg query "HKCR\ADODB.Connection\CLSID" /ve

如果返回一个{...}形式的 CLSID,说明 ADO 组件已注册。如果报“找不到指定的注册表项”,说明 MDAC 组件缺失或被卸载,需要重新安装对应版本。注意不同 Windows 版本自带的 MDAC 版本不同,老系统上常见的是 2.8,但类型库接口仍然兼容 ADO2.20 的调用方式。

3.2 用一段最小脚本验证连接和查询

环境确认后,不要急着写业务代码,先用最小脚本验证连接是否通、查询是否返回预期结果。下面这段代码只做一件事:连上数据库,查一行系统时间。

Dim conn, rs Set conn = CreateObject("ADODB.Connection") conn.Open "Provider=SQLOLEDB;Data Source=127.0.0.1;Initial Catalog=master;User ID=sa;Password=***;" Set rs = conn.Execute("SELECT GETDATE() AS Now") If Not rs.EOF Then WScript.Echo "Server time: " & rs.Fields("Now").Value End If rs.Close conn.Close Set rs = Nothing Set conn = Nothing

逻辑说明:conn.Execute直接返回一个只读向前的Recordset,适合这种一次性查询。参数说明:Data Source可以写 IP 也可以写实例名,如果端口不是默认的 1433,需要写成127.0.0.1,1433的形式。这一步跑通之后,再往上叠加Command和参数化查询,排查范围会小很多。

3.3 记录集遍历时的字段访问方式对比

遍历Recordset时,字段访问有两种写法:rs.Fields("Name").Value和rs("Name").Value。后者是前者的语法糖,但性能上几乎没有差别。真正影响性能的是Fields集合的索引方式:用字符串名称访问比用数字索引慢,因为每次都要做名称查找。如果循环体里要反复访问同一个字段,我一般会先把Field对象取出来:

Dim fldAmount Set fldAmount = rs.Fields("Amount") Do While Not rs.EOF total = total + fldAmount.Value rs.MoveNext Loop

这样在循环内部就不再重复查找字段名。数据量上万行时,这个习惯能省下可观的 CPU 时间。

3.4 事务边界该放在 Connection 还是 Command 上

事务只能由Connection对象开启和提交,Command本身不管理事务。常见做法是:

conn.BeginTrans On Error Resume Next cmd.Execute If Err.Number <> 0 Then conn.RollbackTrans Else conn.CommitTrans End If

注意BeginTrans之后,同一个Connection上的所有Command都处于同一事务中。如果中间某个Command用了不同的连接,事务不会覆盖它。这是老系统里经常出现的“部分提交”问题的根源。

4. 避坑:ADO2.20 使用中最容易翻车的五个场景

4.1 现象:连接池耗尽,报“超出最大连接数”

原因:每次查询都新建Connection且没有显式Close,或者Recordset没有关闭导致连接无法归还池中。ADO 的连接池依赖Connection对象完全释放后才回收物理连接。

解决:确保每个Connection在Finally或错误处理分支里都执行Close,并且把对象设为Nothing。如果用了Recordset,先关Recordset再关Connection。可以在连接字符串里加OLE DB Services=-2显式启用池化,但根本还是靠代码纪律。

4.2 现象:查询大表时内存暴涨,进程被系统杀掉

原因:CursorType用了adOpenStatic或adOpenKeyset,ADO 在客户端物化了整个结果集。数据量几十万行时,内存占用会达到原始数据的好几倍。

解决:只读遍历一律用adOpenForwardOnly加adLockReadOnly。如果确实需要行数,用单独的SELECT COUNT(*)查询,不要靠Recordset.RecordCount。注意adOpenForwardOnly下RecordCount返回 -1,这是正常行为。

4.3 现象:参数化查询报“参数对象未找到”或结果为空

原因:Parameters.Append的顺序和 SQL 里?的顺序不一致,或者字符串参数的长度设得太小导致值被截断。ADO 对参数顺序是敏感的,它不按名称匹配。

解决:按 SQL 中?出现的顺序逐个Append,字符串参数的长度给足实际字段长度。调试时可以先打印cmd.CommandText和每个参数的Value、Size,确认无误再执行。

4.4 现象:事务回滚后,部分数据仍然被写入

原因:事务期间有Command使用了另一个Connection,或者Recordset的LockType是adLockBatchOptimistic但没调用UpdateBatch。

解决:确保事务内的所有操作共用同一个Connection。如果用了批量更新,提交事务前必须显式调用UpdateBatch,否则更改只停留在本地缓存。

4.5 现象:在 64 位系统上调用 ADO 报“类未注册”

原因:ADO2.20 的类型库是 32 位的,64 位进程无法直接加载。很多老代码在 32 位环境下正常,迁移到 64 位就挂。

解决:把宿主进程编译为 32 位,或者改用更高版本的 ADO 类型库。如果必须用 64 位,需要确认系统里注册了对应的 64 位 MDAC 组件,但老版本 ADO 对 64 位的支持并不完整,常见做法是保持 32 位运行环境。

5. 进阶:用连接复用和批量操作把吞吐量提上去

5.1 连接复用不是“一直开着”,而是“用完立刻还”

很多人误以为连接复用就是把Connection对象做成全局单例,一直开着不关。这在单线程脚本里勉强能用,但在多线程或高并发场景下会引发状态污染。正确的做法是:每次操作从池里取一个连接,用完立刻Close,让 ADO 自己决定物理连接是复用还是销毁。连接池的命中率可以通过性能计数器观察,如果命中率长期低于 80%,说明连接持有时间过长。

5.2 批量插入时用 UpdateBatch 替代逐条 Execute

逐条INSERT的开销主要在网络往返和事务日志刷盘。如果一次要插入几百条,可以用adLockBatchOptimistic游标配合AddNew和UpdateBatch:

Dim rs Set rs = CreateObject("ADODB.Recordset") rs.CursorType = 3 ' adOpenStatic rs.LockType = 4 ' adLockBatchOptimistic rs.Open "SELECT * FROM Orders WHERE 1=0", conn Dim i For i = 1 To 500 rs.AddNew rs.Fields("OrderId").Value = i rs.Fields("Amount").Value = i * 10 rs.Update Next rs.UpdateBatch rs.Close

逻辑说明:WHERE 1=0只取结构不取数据,AddNew在本地缓存里追加行,UpdateBatch一次性提交。参数说明:UpdateBatch的参数可以指定冲突处理方式,默认是adAffectAll。注意批量提交时如果某一行违反约束,整个批次可能失败,需要配合错误处理逐行排查。

5.3 用 Command 的 Prepared 属性减少重复解析

如果同一条带参数的 SQL 要执行很多次,可以把Command对象的Prepared属性设为True,让数据提供者缓存执行计划:

cmd.Prepared = True Dim j For j = 1 To 1000 cmd.Parameters("CustomerId").Value = j Set rs = cmd.Execute ' 处理结果 rs.Close Next

这样数据库只需要解析一次 SQL,后续执行直接复用计划。代价是首次执行稍慢,且会占用提供者端的缓存资源。对于执行次数少于 10 次的查询,不建议开启。

5.4 一个验证连接是否真正归还的小技巧

在连接字符串里加上OLE DB Services=-2启用池化后,可以通过查询系统视图确认当前会话数:

SELECT COUNT(*) AS SessionCount FROM sys.dm_exec_sessions WHERE is_user_process = 1

在脚本执行前后各查一次,如果执行后会话数没有回落,说明连接没有正确归还。这个办法比看代码更直接,适合在压测阶段快速定位泄漏点。

我自己的习惯是:任何用到 ADO 的脚本,写完第一件事就是跑一遍“打开-查询-关闭”的循环,用系统视图确认会话数归零,再往上加业务逻辑。这个习惯帮我省掉了至少三次深夜排查连接泄漏的麻烦。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

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