
简介一套用于 VC 开发的 ADO 类库封装源自国外程序员在技术社区发布的早期开源项目目的是把 SQL Server、Oracle、Access 等数据库的访问操作封装成简洁的 C 接口避免开发者直接面对繁琐的 COM 底层细节。压缩包非常精简共 3 个文件两个源代码文件分别存放类声明和功能实现另一份 MHT 网页存档格式的文档保留了原始的使用说明与示例整体仅有 105KB可轻松放入 VC 工程中按需修改。目前已有 125 人学习或下载。通过阅读源码和文档可以学习如何封装数据库连接、命令执行、结果集遍历等常见能力并迁移到自己开发的工具或业务系统中例如用更少的代码建立连接、运行 SQL 语句、读取字段值。对于具备一定 C 基础、希望减少样板代码并快速为桌面应用添加数据读写功能的开发者这是一个有参考价值的实现样例。1. 被当成“老古董”的数据库访问方案ADO 2.20 的 Class 到底还能干什么在很多遗留系统的服务器上数据库访问代码仍然写着ADODB.Connection、ADODB.Recordset这样的 ProgID它们对应的就是 ADO 2.20 的 Class 对象模型。这套类库定义了连接、命令、结果集、字段、错误等一组 COM 类专门解决 Windows 平台上“怎么连数据库、怎么发 SQL、怎么取数据”这三个问题。我接手过好几个无人敢动的报表服务和数据迁移工具底层全靠这套类模型在撑着不是不想换新框架而是业务逻辑和排期都绑死在上面。适合谁看你自己在维护这类老服务或者需要在 Windows 脚本、Python、C# 里快速操作数据库且不想引入重量级 ORM 的人。读完这篇你能照着把连接、查询、更新、调存储过程这条路走通也知道哪些参数一动就翻车。2. 从 Connection 到 RecordsetADO 2.20 的五个核心类与它们的协作方式2.1 类的顶层设计为什么先建 Connection 再建 RecordsetADO 2.20 的对象模型说起来不复杂真正天天打交道的类就五个Connection、Command、Recordset、Field、Parameter外加一个 Error。新手最容易犯的错是跳过 Connection 直接去 Open Recordset确实能跑通因为 Recordset 会自动创建一条隐式连接但这条连接的生命周期完全不受你控制关闭时机、事务边界、连接复用全部变成黑匣子。我一般会显式创建 Connection再把它作为参数传给 Recordset 或 Command。理解这套模型有个类比Connection 是会话通道Command 是预编译好的指令Recordset 是执行结果的内存视图Field 是视图里的列Parameter 是给指令填的参数位。这个分层跟 python 中 class 函数的用法思路一致每个类各自管好自己的状态通过方法参数协作而不是把所有逻辑塞在一个大函数里。把连接建好、把命令对象准备好、最后拿结果集这条链路是 ADO 所有编码方式的主干。2.2 用 Python pywin32 跑通最小连接代码很多人以为 ADO 只能从 VBScript 或 C# 里用实际上 Python 通过 pywin32 的 COM 接口调用起来非常顺排查问题也比脚本语言舒服。下面是连接数据库并读取一张表的最小闭环我日常排查连接问题时就从这段开始改。import win32com.client # 创建连接对象这一步相当于拿到 ADODB.Connection 类的实例 conn win32com.client.Dispatch(ADODB.Connection) conn.ConnectionString ( rProviderSQLOLEDB;Server127.0.0.1; rDatabasedemodb;UIDsa;PWDPassw0rd! ) conn.Open() # 创建结果集对象打开时直接绑定连接和 SQL rs win32com.client.Dispatch(ADODB.Recordset) rs.Open(SELECT id, name FROM users, conn, 1, 1) # 第三个参数是游标类型第四个是锁类型 # 遍历打印结果 while not rs.EOF: print(rs.Fields(id).Value, rs.Fields(name).Value) rs.MoveNext() rs.Close() conn.Close()逐段说明Dispatch 的作用是启动 COM 组件传入的字符串就是 ADO 2.20 类库对外暴露的 ProgID类名必须是ADODB.开头。Open的第三、第四个参数分别对应 CursorType 和 LockType这里用了 1 和 1表示键集游标加只读锁是只读查询最省事的组合。遍历时判断EOF属性是否到达末尾Fields(列名).Value取当前行字段值最后务必按先结果集后连接的顺序关闭顺序反了容易出现“对象未打开”的假异常。2.3 参数说明游标类型与锁类型的选择Open 方法里那两个数字是 ADO 里最容易让人懵的参数。游标类型决定结果集能往前还是往后走、能否看到别人提交的新数据锁类型决定更新时数据库以什么方式保护数据。下表是我自己常用的速查口径。游标类型常量值能力与代价adOpenForwardOnly0只能向下走性能最好适合一次性报表adOpenKeyset1可前后移动看不到别人新增的行适合带书签的翻页adOpenDynamic2可前后移动且实时看到变动代价是连接压力大adOpenStatic3数据快照移动自由适合排序和缓存锁类型常量值适用场景adLockReadOnly1只读查询最安全adLockPessimistic2编辑时立即锁行写操作首选adLockOptimistic3只在 Update 时锁行适合批量修改adLockBatchOptimistic4配合批量更新适合离线改完一起提交组合上有个原则要更新就选乐观或悲观锁不要用只读锁去调用 Update否则报错没商量要翻页就避开 ForwardOnly不然 MovePrevious 直接抛异常。参数写完之后再动代码能省掉一半排错时间。3. 动手操作数据Command、Recordset 与存储过程的三种标准玩法3.1 用 Command 执行参数化 SQL避免字符串拼接SQL 注入和中文引号错乱几乎都是字符串拼接惹的祸。ADO 里处理参数化查询的正规姿势是用 Command 类给 SQL 里的占位符准备 Parameter 对象再把值绑定进去。这样既安全数据库还会缓存执行计划重复执行时性能更好。cmd win32com.client.Dispatch(ADODB.Command) cmd.ActiveConnection conn # 复用前面创建的连接 cmd.CommandText SELECT * FROM users WHERE dept_id ? AND age ? # 创建两个参数对象并追加到 Command 的参数集合里 p1 cmd.CreateParameter(dept_id, 3, 1, 8, 10) # 类型 3 是整型方向 1 是输入 p2 cmd.CreateParameter(age, 3, 1, 8, 25) cmd.Parameters.Append(p1) cmd.Parameters.Append(p2) rs cmd.Execute() while not rs.EOF: print(rs.Fields(name).Value) rs.MoveNext() rs.Close()代码逻辑不复杂先设CommandText为带问号占位符的 SQL然后按占位符顺序创建参数。CreateParameter的五个参数分别是名称、数据类型、方向、长度、值。类型 3 是 adInteger方向 1 是 adParamInput长度 8 对应 4 字节整型宽度。如果你用了 Oracle 或别的数据库类型编号会不一样最稳妥的办法是先查驱动文档再填。参数化之后SQL 文本里不再直接出现具体值注入和转义问题从根上断掉。3.2 Recordset 遍历与批量更新三种常见误用Recordset 的用法很灵活但常见误用也特别多我列三个翻车频率最高的。第一种是在只读游标上做更新。有人图省事用rs.Open(sql, conn, 1, 1)查出数据后直接调rs.Fields(age).Value 30再rs.Update()结果抛“当前记录集不支持更新”。原因就是锁类型选了只读解决方法是把第四个参数改成 2 或 3。第二种是在EOF状态下读字段。结果集为空时rs.Fields也能取对象但.Value会抛“列不存在或不可用”。很多人排查半天不知道数据是空的其实是先判断rs.EOF再取值顺序倒过来就会踩坑。第三种是在循环里反复Open同一个结果集对象。每次 Open 都会重新向数据库申请游标和锁循环一千次就建立一千次会话服务端压力直接拉满。正确做法是一次性 Open用MoveNext走完或者用GetRows把数据全部拉进数组再循环。这三种误用也是我在帮别人 review 老代码时最常见的三个问题。3.3 调存储过程返回多个结果集时如何拿到第二个 Recordset存储过程里常有“先查汇总再查明细”的逻辑一次调用返回多个结果集。ADO 处理这种情况要用NextRecordset方法它会关闭当前结果集把连接切换到下一个结果集上。proc win32com.client.Dispatch(ADODB.Command) proc.ActiveConnection conn proc.CommandText get_user_and_orders # 这里假设存储过程已存在 proc.CommandType 4 # adCmdStoredProc告诉 ADO 这是存储过程 rs proc.Execute() print(第一个结果集) while not rs.EOF: print(rs.Fields(0).Value) rs.MoveNext() # 切换到第二个结果集 rs2 rs.NextRecordset() if rs2 is not None: print(第二个结果集) while not rs2.EOF: print(rs2.Fields(0).Value) rs2.MoveNext() rs2.Close() rs.Close()CommandType 4是关键它让 ADO 不再把 CommandText 当普通 SQL 解析而是直接按存储过程名调用。NextRecordset返回 None 时表示没有更多结果集。需要注意即使第一个结果集没读完调 NextRecordset 也会强制把当前游标推完并跳到下一个所以如果你只关心第二个结果集也要先过一遍第一个否则驱动行为可能不符合预期。多结果集处理在报表类需求里非常常见这条路径值得记熟。4. 性能与边界游标、锁、连接池的取舍以及该调的几个参数4.1 客户端游标与服务端游标的性能差异游标位置是 ADO 性能调优里最被忽略的参数它由 Connection 的CursorLocation属性控制取值为 2 表示服务端游标3 表示客户端游标。服务端游标把游标状态放在数据库进程里每次 MoveNext 都可能发生一次网络往返但内存占用小客户端游标会把整个结果集拉到应用进程内存中后续翻页和排序都在本地完成网络压力小但内存开销大。大数据量查询比如几十万行导出场景我一般选客户端游标因为在服务端保持一个几十万行的游标数据库服务器的临时空间和锁资源都会非常难看。小数据量且需要实时更新的场景服务端游标更合适。一个容易踩的坑是客户端游标的记录集在连接关闭后仍然可以读取字段服务端游标在连接关闭后立即失效。这直接关系到代码里关闭连接的顺序。4.2 LockType 与 CursorType 的组合关系表游标类型和锁类型不是自由组合的某些组合在特定驱动下直接不支持。下面是我在 SQLOLEDB 驱动下验证过的可用组合作为默认起点。游标类型只读锁 1悲观锁 2乐观锁 3批量乐观锁 4ForwardOnly 0可用驱动相关可用不可用Keyset 1可用可用可用可用Dynamic 2可用可用可用不可用Static 3可用可用可用可用这里说的“可用”指大多数驱动能正常执行不代表逻辑正确。以只读锁为例它能和所有游标类型配合但在它下面执行 Update 一定失败所以选型时先问自己这块数据是只读还是可写可写就不要用只读锁。批量乐观锁配合 Keyset 游标适合网格控件里一次性改一堆行再统一提交配合 UpdateBatch 使用但 ForwardOnly 和 Dynamic 游标不支持这种模式用了就会出现“操作不被支持”的错误。拿不准时Keyset 游标加乐观锁是我最常用的组合能读能改性能也在可接受范围。4.3 排查“类未注册”与“类型不匹配”把 -verbose:class 思维带进 COM 排查熟悉 Java 的同学知道-verbose:class能打印类加载过程帮你看清楚某个类到底从哪个 jar 里来、是否被覆盖。排查 ADO 组件时也应该有同样的“类加载”意识程序报“ActiveX 组件不能创建对象”或“类未注册”十有八九是注册表里 ADODB 相关组件的 ProgID 指向出了问题。高版本组件覆盖、系统精简、32 位与 64 位注册表重定向都会让 ADO 2.20 类库“失踪”。我一般先手动创建对象看具体卡在哪一层# 直接创建连接对象复现完整报错 $conn New-Object -ComObject ADODB.Connection $conn.ConnectionString ProviderSQLOLEDB;Server127.0.0.1;Databasedemodb;UIDsa;PWDPassw0rd! $conn.Open() Write-Output 连接成功如果创建对象这一步就失败再去注册表里检查类是否真的存在# 查看 ADODB.Connection 的注册项是否存在 Get-Item HKLM:\SOFTWARE\Classes\ADODB.Connection -ErrorAction SilentlyContinue | Format-List * # 查看本机可用的 OLEDB Provider 清单 Get-ChildItem HKLM:\SOFTWARE\Classes | Where-Object { $_.PSChildName -like ADODB.* } | Select-Object -ExpandProperty PSChildName第一段代码用 PowerShell 的New-Object -ComObject在独立进程里复现问题能区分是代码问题还是环境问题。如果创建成功但 Open 失败说明组件正常问题在连接字符串参数上。第二段代码直接查看注册表项确认 ADODB 类是否真实存在以及是否被其他版本覆盖。这里还能发现一个老坑32 位 ADO 组件在 64 位环境下注册表路径是在HKLM:\SOFTWARE\WOW6432Node\Classes下查错路径就会误判成组件丢失。5. ADO 2.20 避坑指南类型不匹配、游标失效与内存泄放的真实案例5.1 现象结果集打开后马上关闭连接读取字段报“对象关闭或不可用”一段看起来很合理的代码conn.Open()、rs.Open()、conn.Close()然后再去遍历 rs 取名结果抛“对象关闭或不可用”。原因是 Recordset 默认依赖连接提供数据服务连接一关游标就断了。解决方法是分情况取舍数据行数不大时在关连接前调rs.GetRows()把数据复制到数组里再关数据量大时用客户端游标CursorLocation3它会把数据缓存到本地内存关连接后仍可读取。这个错误的迷惑性在于不是每次都会稳定复现取决于驱动和游标类型容易被当成玄学实际上是连接生命周期管理没做好。5.2 现象读出来的中文字段变成问号或乱码从数据库读出的中文变成“???”或者显示成明显错位的字符。原因通常是 OLEDB 驱动版本与数据库排序规则不匹配或者 Python 进程的默认编码与 ADO 返回的 BSTR 字符串转换出问题。这条路我走过很多次最后发现多数情况不是 ADO 的锅而是输出环节的编码不对。解决方法是先把取到的值打印一下类型和 Unicode 编码确认数据在 ADO 层面是否正常如果数据正常再检查终端或日志的编码设置例如 Python 里sys.stdout.reconfigure(encodingutf-8)。如果把值写进文件是乱码那就是写入文件时用的编码问题跟 ADO 无关。判断数据源乱码还是输出乱码最笨但最有效的方法是print(repr(rs.Fields(0).Value))看它内部表示是否包含\u开头的正常转义。5.3 现象程序跑一段时间后连接超时数据库连接数被打满这是血泪经验。某个定时任务最初运行正常跑了半个多月后开始频繁报连接超时数据库端连接数满。定位发现代码里每次查询都 new 一个 Connection 和 Recordset循环结束后既不 Close 也不置空COM 对象在 Python 的垃圾回收机制下不会立刻释放连接就被占着不放。解决方法是把连接和结果集的关闭逻辑放到try/finally里确保无论如何都会 Close并且循环内尽量复用同一个连接不要每次查询都新建。还有一点Python 用 win32com 创建 COM 对象时部分对象会在进程退出时才真正释放所以在长时间运行的服务里显式调Close()比依赖 GC 可靠得多。我把这个习惯固化成了模板所有写库逻辑必须带 finally 关闭宁可多写两行也不赌回收时机。5.4 现象重写包装类方法后调用父类报“参数数目错误”类似 overrider 注解丢失的问题有人喜欢把 ADO 封装成自己的数据层类比如继承一个 Recordset 包装类去扩展功能。重写open方法时只写了sql参数复用旧代码调用时却报“参数数目错误”。这很像 class 文件 overrider 注解为什么会丢失那个场景源文件里明明写好了重写注解编译后却找不到了。COM 对象的方法签名是通过 IDispatch 接口暴露的调用方按签名顺序传参一旦重写后的方法参数列表比原方法少派发就会失败。解决方法是重写时保留原方法的完整参数签名哪怕某些参数在子类里用不到也要接着更稳妥的做法是不重写父类方法而是新增一个命名不同的方法避免覆盖内部派发表。这个坑提醒我操作 COM 组件时不要用普通面向对象的直觉去猜方法重写行为接口签名就是合同少一个参数等于违约。6. 进阶技巧用 ADODB.Stream 处理二进制字段以及把 ADO 封装成自己的数据访问类6.1 用 ADODB.Stream 存取图片文件数据库里存图片或附件时字段类型通常是 varbinary 或 image直接常规读写会遇到类型转换问题。ADO 处理二进制数据的标准工具是 ADODB.Stream 类它的 Type 属性置 1 表示二进制模式置 2 表示文本模式。下面是一段把图片字段读出来存成文件的示例。stream win32com.client.Dispatch(ADODB.Stream) stream.Type 1 # adTypeBinary stream.Open() # rs 是已打开并定位到目标行的 Recordset data rs.Fields(photo).Value if data: stream.Write(data) stream.SaveToFile(C:\\demo\\out_image.bin, 1) # 1 表示覆盖已存在文件 stream.Close()逻辑说明Stream 对象先设成二进制模式Open 之后没有任何数据接着把 Recordset 里 photo 字段的值直接 Write 进去最后 SaveToFile 落盘。第二个参数 1 是 adSaveCreateOverWrite表示文件存在就直接覆盖如果要保留历史版本可以改成 0。反过来写库也是一样的思路先用 Stream 从文件 LoadFromFile再读出来赋给字段。二进制的坑主要在字段值为空时直接调 Write 会报错所以加了一层if data判断。6.2 把常用操作封装成类像定义 python class 一样组织 ADO 调用代码写多了之后我习惯把连接管理和查询逻辑收拢成一个类避免每个脚本里重复写 Dispatch、Open、遍历、Close 这一套。封装思路跟定义普通 Python 类完全一致只是内部通过 COM 调 ADO。class AdoHelper: def __init__(self, conn_str): self.conn win32com.client.Dispatch(ADODB.Connection) self.conn.ConnectionString conn_str self.conn.Open() def query_all(self, sql): rs win32com.client.Dispatch(ADODB.Recordset) rs.Open(sql, self.conn, 1, 1) rows [] try: while not rs.EOF: row [rs.Fields[i].Value for i in range(rs.Fields.Count)] rows.append(row) rs.MoveNext() finally: rs.Close() return rows def close(self): if self.conn: self.conn.Close()这个类只做两件事构造时建连接query_all 里查数据并返回二维列表。关键点在 finally 里关闭 Recordset保证异常时也能释放。用的时候先db AdoHelper(conn_str)再rows db.query_all(SELECT * FROM users)最后db.close()。参数化查询、多结果集还能继续往这个类里加方法但注意前面说的重写签名问题扩展用新增方法不要覆盖。我自己的习惯是每个工具脚本里都内置一个简化版 Helper连接字符串放配置绝不明文写死在代码里数据量大时再单独加分页方法不要在一个方法里既查数据又写日志。这样维护老系统时新功能可以快速落地老逻辑也不会被反复重写。希望帮到你。本文还有配套的精品资源点击获取