1. ADO 卡在技术栈的哪个位置
ADO,全称 ActiveX Data Objects,是 Windows 平台上那套基于 COM 的数据访问组件。抛开这些名词,一句话概括:它负责在应用代码和数据库之间搭一条管道,让开发者用几乎同样的写法去访问不同厂商的数据库。只要你写过 ASP 页面、维护过 VB6 或 VBA 时代的老系统、修过 Excel 宏里那段连接数据库的代码,那你大概率已经跟 ADO 打过交道,只是没细究过它内部长什么样。我最近接手一套跑了十几年的订单处理系统,从上到下全是 ADO:几千行 VBScript 拼 SQL、几十个 Recordset 层层嵌套、连异常处理都靠On Error Resume Next加If conn.Errors.Count > 0硬扛。刚看完代码那天晚上我甚至想把这套东西整体换掉,但把业务方的停机窗口、改造成本、验证周期盘了一遍之后,结论很现实:动不起,不如把它吃透。
这篇文章面向三类人。第一类是不得不维护 ADO 遗留系统的人,你可能刚从现代框架转过来,看到CreateObject("ADODB.Connection")会觉得它像出土文物,但业务还在跑,你得先看懂它,再谈改造。第二类是要在 VBA、ASP、Windows 桌面程序里做轻量数据集成的人,这类场景里 ADO 依然是阻力最小的方案,没有中间件、没有 ORM,一个连接对象加一条 SQL 就能开工。第三类是对数据访问演进史感兴趣的人,理解了 ADO 的对象模型、游标机制和锁模型,你再去看今天各种数据访问库的设计取舍,会发现很多思路是相通的,只是包装换了。
先把最容易被误解的一点说清楚:ADO不是数据库驱动,它是一层抽象。真正的活儿是下层的 OLE DB Provider 干的,ADO 只负责把你的调用翻译成 Provider 能听懂的请求,再把结果包装成对象返回。这个分层解释了后面几乎所有的怪现象——为什么同一个错误码在不同数据库上表现不一样,为什么有的属性设了没反应,为什么连不上时错误信息有时特别含糊。你把这层关系想明白,排查问题时就不会在 ADO 这一层空转。
ADO 解决的原始痛点其实很朴素:在它出现之前,开发者要用 ODBC API 直接跟 C 接口打交道,分配句柄、绑定缓冲区、逐行取数据,一套流程写下来几十行,换个数据库还要改。ADO 把这套流程对象化,Connection 管连接、Command 管指令、Recordset 管结果集,门槛一下降到"会写脚本就能取数"。这不是炫技式的抽象,是实打实的生产力提升,也是它能活这么久、至今还在大量存量系统里服役的根本原因。
注意:本文所有示例中的地址、库名、账号均为示意占位,请按自己的环境替换,别直接抄进生产环境。
2. 对象模型拆解:七个对象各管一段
2.1 三件套的分工边界
ADO 的对象模型一共七个:Connection、Command、Recordset、Field、Parameter、Error、Property。前三个是主干,后四个是支撑。我见过太多代码把这三个对象的职责揉成一团——用 Connection 直接执行 SQL、用 Recordset 反复开关连接、在 Command 里塞几十个拼出来的字符串。能跑,但一旦出问题极难定位。
正确的分法是这样的:Connection 只负责"在哪连"和"连得上连不上",它的生命周应该跟应用生命周期对齐,而不是跟某次查询对齐;Command 负责"发什么",参数化、存储过程、超时设置都归它管;Recordset 负责"拿回来什么",游标类型、锁定方式、批量更新策略在这一层决定。三者各守一段,调试的时候你才能快速判断问题出在链路、语句还是游标。
| 对象 | 核心职责 | 典型误用 | 排查信号 |
|---|---|---|---|
| Connection | 建立/维持连接,管理事务 | 每次查询都新建再关闭 | 连接数暴涨、握手耗时占比高 |
| Command | 承载 SQL 或存储过程、参数 | 用字符串拼接代替参数 | SQL 注入风险、执行计划复用率低 |
| Recordset | 承载结果集、控制游标与锁 | 一律用默认参数打开 | 内存占用高、并发更新冲突 |
| Field | 描述单列元数据与值 | 按序号硬取列 | 加字段后下标错位 |
| Parameter | 描述命令参数 | 类型长度不匹配 | 类型转换异常、精度丢失 |
| Error | 累积最近一次操作的错误 | 只判断单一错误 | 多层错误被覆盖漏报 |
| Property | 动态特性开关 | 盲目设置无效属性 | 设置了但行为没变 |
2.2 被忽略的 Field 与 Error
Field 对象看着不起眼,但它承载了列名、类型、长度、是否可空这些元信息。实际项目里我踩过最惨的一次坑就是按序号取值:rs.Fields(3).Value,代码写了两年没人动,后来业务加了一个字段插在中间,整个报表的列全错位,而且不报错——因为类型刚好兼容,数据静默错乱。从那以后我在团队里立了条规矩:除非是SELECT *的临时脚本,否则一律按列名取值,rs.Fields("order_amount").Value,多敲几个字符,省掉一次线上事故。
Error 对象更容易被忽视。它挂在 Connection 上,是个集合而不是单个对象,一次操作可能累积多条错误。很多人只写If Err.Number <> 0就完事,结果真正来自数据库的错误描述被前面的通用错误盖掉了。正确的姿势是把conn.Errors整个遍历一遍,每条的 Number、Description、Source、SQLState 都打出来,日志里才有可查的东西。
On Error Resume Next conn.Execute "UPDATE orders SET status = 2 WHERE id = " & orderId If conn.Errors.Count > 0 Then Dim e For Each e In conn.Errors WriteLog "ADO_ERROR", e.Number & " | " & e.Source & " | " & e.Description & " | " & e.SQLState Next conn.Errors.Clear End If这段代码的价值不在于它能捕获错误,而在于它留下了现场。上线之后出问题,日志里能看到完整的错误链,不用靠猜。
2.3 对象之间怎么串起来
三个主干对象的组合方式有三种,选哪种取决于你的场景。
第一种是 Connection 直连执行,conn.Execute "SELECT ...",返回一个只读向前的 Recordset。适合一次性脚本、行数很少的查询、DDL 语句。缺点是没法参数化,也没法设单独的 CommandTimeout。
第二种是 Command 挂在 Connection 上执行,能参数化、能调存储过程、能控制超时。这是生产环境的主力写法,除了极简单的语句,我基本都用这个。
第三种是 Recordset 自己Open时带上 SQL 和连接,本质上是第二种的语法糖,底层还是走 Command。新手爱用,因为它短,但在复杂场景下不便于复用命令对象。
' 写法一:Connection 直连,适合简单查询 Dim rs1 Set rs1 = conn.Execute("SELECT TOP 10 id, order_no FROM orders ORDER BY id DESC") ' 写法二:Command 参数化,生产推荐 Dim cmd Set cmd = CreateObject("ADODB.Command") Set cmd.ActiveConnection = conn cmd.CommandType = 1 ' adCmdText cmd.CommandTimeout = 30 cmd.CommandText = "SELECT id, order_no FROM orders WHERE status = ? ORDER BY id DESC" cmd.Parameters.Append cmd.CreateParameter("status", 3, 1, , 2) ' adInteger, adParamInput Set rs1 = cmd.Execute两种写法跑出来的结果一样,但第二种在参数类型、超时控制、执行计划复用上都更稳。这一点在后面讲参数化的时候还会展开。
3. 连接层实操:连接字符串与生命周期
3.1 连接字符串究竟在说什么
连接字符串是 ADO 里最容易抄错、也最难排查的一段配置。它本质上是传给下层 Provider 的键值对,ADO 自己只认几个通用键,其余全是 Provider 私有的。这意味着你看文档时得先搞清楚:哪个键是 ADO 的,哪个键是 Provider 的。
Dim conn Set conn = CreateObject("ADODB.Connection") conn.ConnectionString = "Provider=SQLOLEDB;" & _ "Data Source=10.0.0.12,1433;" & _ "Initial Catalog=OrderDB;" & _ "User ID=app_reader;" & _ "Password=******;" & _ "Connect Timeout=15;" & _ "Application Name=OrderSvc" conn.CommandTimeout = 30 conn.Open几个参数值得单独说。
Provider决定了下层用哪个驱动,选错了后面全错。同一个数据库可能有多个 Provider 可选,性能和特性支持不同,选型时要实测,别只看文档。
Connect Timeout是建立 TCP 连接加认证的时间上限,默认 15 秒。这个值我给的建议是压到 5 到 10 秒。理由很直接:如果数据库真的不可达,等 15 秒再失败毫无意义,只会把调用方的线程一起拖住。在高峰时段,几十个请求卡在握手上,服务就雪崩了。
Application Name是很多人不写的,但它对排查极其有用。数据库侧的会话视图里能直接看到这个标识,出问题时你一眼就知道是哪个应用、哪个模块在占连接。多应用共用一个库的时候,这个字段能省掉大量扯皮。
密码不要硬编码在脚本里,这是老生常谈,但我在遗留系统里见过太多次。最低限度用配置文件加权限控制,条件允许就上进阶的凭据管理方案。
3.2 连接的打开与关闭时机
关于连接什么时候开、什么时候关,业界吵了很多年。早期的建议是"晚开早关",因为那时候连接是稀缺资源,数据库许可按并发连接数卖。后来连接池普及了,这条建议就变得有点教条。
我的实践原则是:按业务事务边界划连接生命周期。一个业务操作涉及多次查询、需要保证一致性,那就开一个连接、走完、关掉。反过来,如果一个操作只有一次查询,那开一个连接查完就关,交给池子去复用,也没什么不好。
真正要避免的是两种极端。一种是每次rs.Open都新建连接,一个页面下来开十几个连接,池子频繁打满又释放,性能全耗在握手和认证上。另一种是全局一个静态连接永不关闭,看着省事,实际上连接一旦进入异常状态(网络抖动、数据库重启、会话被踢),整个应用就永久性失联,只能重启进程。
' 一个业务事务内复用连接 conn.BeginTrans On Error Resume Next conn.Execute "UPDATE inventory SET qty = qty - 1 WHERE sku = 'A1001' AND qty > 0" conn.Execute "INSERT INTO order_items(order_id, sku, qty) VALUES (10086, 'A1001', 1)" If conn.Errors.Count = 0 And Err.Number = 0 Then conn.CommitTrans Else conn.RollbackTrans WriteLog "TX_FAIL", Err.Description End If On Error GoTo 0这段代码里有个细节值得注意:UPDATE的条件带了AND qty > 0,靠数据库的行级锁保证不超卖,而不是先查再改。先查再改在并发下必然出问题,这是库存类业务里最经典的坑,跟 ADO 无关,但用 ADO 写这类逻辑时特别容易犯。
提示:事务对象挂在 Connection 上,跨连接的事务 ADO 管不了。如果你的逻辑需要跨库事务,得上升到分布式事务的层面,那是另一个复杂度,别指望在 ADO 层解决。
3.3 连接池不是万能药
连接池确实省掉了大量握手开销,但它有个前提:连接必须能被干净地归还。如果代码里有分支忘记Close,或者异常路径跳过了关闭,连接就泄漏了。表现出来是系统跑一段时间后突然大量报"连接超时"或者"达到最大池容量",重启就好,过几天又犯。
排查这类问题的土办法很有效:在数据库侧看会话数随时间的变化曲线,正常应该是平稳的锯齿状,泄漏则是一条持续上升的斜线。定位到应用后,把Open和Close成对检查一遍,重点看On Error Resume Next之后有没有Exit Function这种提前返回。
还有一个坑是连接状态污染。某些会话级设置(比如隔离级别、日期格式、语言)在连接归还后不会自动重置,下一个借到这条连接的请求会继承上一个请求的设置,行为诡异且难复现。解决办法是在借出连接后显式跑一遍初始化语句,或者干脆避免在会话级别改这些东西。
4. Recordset 的游标与锁:最容易被默认值坑的地方
4.1 游标类型到底影响什么
Recordset 打开时如果不显式指定游标类型,ADO 会给你一个默认值。这个默认值在简单查询上没问题,但在复杂场景下会带来性能和行为上的双重意外。所以我给的建议很直接:凡是生产代码,Open的第三第四个参数一律显式写出来,别依赖默认。
游标类型决定了三件事:能不能前后滚动、能不能看到别人的修改、以及服务端和客户端谁在维护这份结果集。
| 游标类型 | 常量值 | 可滚动 | 可见他人修改 | 结果集存放 | 适用场景 |
|---|---|---|---|---|---|
| 仅向前 | 0 | 否 | 否 | 服务端流式 | 一次性遍历、导出、大批量读 |
| 静态 | 3 | 是 | 否 | 服务端快照 | 需要反复遍历、做统计 |
| 键集 | 1 | 是 | 部分 | 服务端存键 | 边读边改、数据量中等 |
| 动态 | 2 | 是 | 是 | 服务端 | 强实时性、小结果集 |
| 客户端静态 | 3 + CursorLocation=3 | 是 | 否 | 客户端内存 | 断开连接后继续用 |
表格看着抽象,落到实际选型上其实很简单。只读一遍就丢的,用仅向前,这是最快也最省内存的,导出十万行报表就用它。需要在结果集里来回跳、反复统计的,用静态游标,代价是数据库要维护快照。需要边遍历边更新、且数据量不大的,用键集游标。结果集小、对实时性要求极高、多人同时改的场景,才考虑动态游标,它开销最大,滥用会很痛。
我这几年见过最典型的性能事故,是一个列表页用了默认游标去查一张千万级表。页面只显示前 20 条,但数据库老老实实把整个结果集建好了游标。查询本身 200 毫秒,建游标花了 40 秒,然后超时。把游标改成仅向前、SQL 里加上TOP 20,问题当场消失。
4.2 锁定类型与并发冲突
锁定类型决定了你改数据时数据库怎么防冲突。四种取值:只读、悲观锁、乐观锁、批量乐观。
只读锁最省事,什么锁都不加,适合纯查询。悲观锁在读取瞬间就把行锁住,别人改不了,直到你更新或释放。它的一致性最强,但并发能力最差,一旦客户端卡住,锁就一直挂着,很容易演变成锁等待甚至死锁。乐观锁读的时候不加锁,更新时才检查版本,冲突了报错让你重试,这是互联网场景的主流选择。批量乐观是乐观锁的批量版本,配合客户端游标和UpdateBatch使用。
' 只读 + 仅向前,列表导出场景的最优解 Dim rsList Set rsList = CreateObject("ADODB.Recordset") rsList.Open "SELECT id, order_no, amount FROM orders WHERE dt = '2024-06-01'", conn, 0, 1 ' 客户端游标 + 批量乐观锁,适合断开后编辑再回写 Dim rsEdit Set rsEdit = CreateObject("ADODB.Recordset") rsEdit.CursorLocation = 3 ' adUseClient rsEdit.LockType = 4 ' adLockBatchOptimistic rsEdit.Open "SELECT id, qty FROM order_items WHERE order_id = 10086", conn, 3, 4 Set rsEdit.ActiveConnection = Nothing ' 断开,数据留在内存 ' ... 用户在本地改数据 ... Set rsEdit.ActiveConnection = conn ' 重新接上 rsEdit.UpdateBatch断开式记录集是 ADO 一个很实用的特性,尤其在网络不稳定或者需要长时间让用户编辑的场景下。但它有个坑:断开期间数据可能被别人改了,UpdateBatch时会产生冲突记录,你得遍历rs.Filter = 2(只显示冲突行)逐条处理,或者干脆放弃批量、改成逐条带版本号更新。很多代码直接调UpdateBatch不管返回值,冲突静默丢失,数据就这么错了。
注意:
UpdateBatch之后一定要检查rs.Filter和每条记录的Status,冲突处理逻辑不写,等于埋雷。
4.3 分页别再用 Recordset 自带的那套
ADO 的 Recordset 支持AbsolutePage和PageSize做分页,看着很方便,但它底层的实现是先取全量再定位,数据量大时性能灾难。我在一个老项目里见过用这招分页查十万行表,每翻一页要几秒钟,翻到后面越来越慢。
正确做法是把分页推给数据库,用ORDER BY ... OFFSET ... FETCH或者按主键游标分页,只取当前页需要的那几十行。ADO 在这件事上只是个搬运工,它不该承担分页逻辑。
按主键游标分页的写法在 ADO 里也很自然:
' 上一页最后一条的 id 是 lastId Dim sql sql = "SELECT TOP 20 id, order_no FROM orders WHERE id > " & lastId & " ORDER BY id"这种写法比 OFFSET 分页更稳,翻到深页也不会变慢,因为它是索引 seek 而不是扫描跳过。前提是排序字段有索引且唯一,不然会漏行或重复。
5. Command 与参数化:把注入挡在门外
5.1 参数对象为什么值得多写几行
字符串拼接 SQL 的危害不用多讲,但很多人对参数化的理解停留在"防注入"。实际上它还有两个更常被忽略的收益:类型正确和执行计划复用。
类型正确这一点在日期和数值上尤为明显。拼接出来的'2024-06-01'到数据库那边是字符串,要经过隐式转换,转换规则受会话的日期格式设置影响,换个语言环境、换个客户端工具就可能解析失败。参数化之后类型明确传递,转换在驱动层完成,行为稳定。
执行计划复用则是性能层面的。数据库对参数化语句可以缓存执行计划反复使用,而对拼接出来的每条不同字面量的 SQL,都得重新解析、编译。高频调用的语句,这个差异能到几倍。
Dim cmd Set cmd = CreateObject("ADODB.Command") Set cmd.ActiveConnection = conn cmd.CommandType = 4 ' adCmdStoredProc cmd.CommandText = "usp_query_orders" cmd.CommandTimeout = 20 cmd.Parameters.Append cmd.CreateParameter("start_dt", 7, 1, , startDate) ' adDate cmd.Parameters.Append cmd.CreateParameter("end_dt", 7, 1, , endDate) cmd.Parameters.Append cmd.CreateParameter("status", 3, 1, , statusVal) ' adInteger cmd.Parameters.Append cmd.CreateParameter("page_no", 3, 1, , pageNo) cmd.Parameters.Append cmd.CreateParameter("page_size",3, 1, , 20) cmd.Parameters.Append cmd.CreateParameter("total_cnt",3, 2, , 0) ' adParamOutput Set rs = cmd.Execute totalCount = cmd.Parameters("total_cnt").Value这里CreateParameter的五个参数依次是名称、类型、方向、大小、值。日期类型的"大小"可以留空,整数也是,但字符串必须给长度,否则驱动可能按默认长度截断,这个坑我踩过一次——传了个 300 字符的备注,落库只剩 50 字符,查了半天才发现是参数长度没写。
5.2 输出参数与返回值的处理顺序
调存储过程时有个顺序问题必须注意:Execute之后才能读输出参数的值,而且要在 Recordset 关闭之前读。因为某些驱动在结果集关闭时会重置参数集合,你再去读就是空值。
另外,如果存储过程同时返回结果集和输出参数,cmd.Execute返回的是第一个结果集,后续结果集要用rs.NextRecordset拿。这个链式结构写起来不难,但容易漏,尤其是存储过程里加了调试用的SELECT之后,整个链就错位了——你以为拿到的是业务数据,其实是那句调试输出。所以我给团队的要求是:存储过程里禁止留裸 SELECT 做调试,要调试就写日志表。
5.3 命令超时与取消
CommandTimeout设多少合适,没有标准答案,但有个判断方法:把这条语句在高峰期的 P99 耗时乘 2 到 3,就是合理上限。设太短会误杀正常请求,设太长会让慢查询把线程池占死。
还有一个不太常用的能力是cmd.Cancel,可以在另一个线程里中断正在执行的命令。这在有用户交互的场景里很有用——用户点了取消,或者页面已经关了,让数据库继续跑那条重查询纯属浪费。不过要注意,取消是异步的,调用后不一定立刻生效,还得靠超时兜底。
6. 常见问题与排查实录
6.1 错误码速查与定位思路
ADO 的错误会分几层冒出来:ADO 自己的错误、Provider 的错误、数据库返回的错误。conn.Errors集合里通常能看到完整的链,从最外层到最内层。看的时候要从最后一条往前读,最内层那条才是根因。
| 现象 | 常见根因 | 排查动作 |
|---|---|---|
| 连接超时 | 网络不通、实例未监听、防火墙拦截 | 先用同环境的命令行工具连一次,排除代码问题 |
| 登录失败 | 账号密码错、认证模式不匹配、账号被锁 | 确认认证方式;检查账号状态而非只改密码 |
| 对象名无效 | 库名或 schema 不对、权限不足 | 用同一账号手工执行一遍 SQL |
| 参数类型不匹配 | 参数类型常量写错、长度截断 | 打印参数集合逐项核对 |
| 连接池耗尽 | 分支未关闭、异常路径泄漏 | 看会话数曲线,检查提前 return |
| 死锁牺牲者 | 加锁顺序不一致、事务过长 | 缩短事务、统一访问顺序 |
| 中文乱码 | 字符集不匹配、参数长度按字节算 | 核对库与连接的字符集设置 |
| 更新影响 0 行 | WHERE 条件不匹配、并发覆盖 | 打印受影响行数,别只看有没有报错 |
有一条经验我想单独强调:Execute返回的影响行数一定要检查。很多业务逻辑是"更新成功就继续",但更新 0 行也算成功,不报错。库存扣减、状态流转这类操作,如果没检查行数,就会出现数据看起来更新了其实没更新的诡异情况。教训是花了一次线上对账才换来的。
6.2 几个反直觉的现象
现象一:查询在数据库客户端很快,在应用里很慢。十有八九是参数类型不一致导致索引失效。客户端里字面量'10086'是字符串,应用里传的可能是整数,或者反过来,隐式转换让索引走不了。解决办法是把参数类型和列类型严格对齐,然后看执行计划确认。
现象二:偶发的"连接繁忙"错误。通常是同一个 Connection 对象被多个线程共用。ADO 的 Connection 不是线程安全的,跨线程共享必然出问题,而且偶发、难复现。规则很简单:一个线程一个连接,别想着省。
现象三:第一次调用慢,后面快。这是正常的冷启动,包括连接建立、计划编译、缓存预热。但如果每次重启都慢很久,可能是连接池的初始容量设得太小,或者连接字符串里有会阻止池化的键。部分 Provider 在看到某些连接字符串键时会禁用池化,具体哪些键各家不同,改配置后要实测验证。
现象四:SELECT COUNT(*)特别慢。大表上的精确计数本来就慢,跟 ADO 没关系。如果只是分页需要总数,考虑用近似值、缓存,或者改成"是否有下一页"的判断方式,能省掉大量开销。
6.3 性能调优的几个抓手
调优这件事,先定位再动手,别上来就改参数。我一般的顺序是:先看单条语句的执行耗时分布,找出最慢的那几条;再看连接获取耗时,判断是不是池化有问题;最后看结果集大小,判断是不是取了不需要的数据。
最容易被忽略的是最后一条。SELECT *在大宽表上拉回来的数据量可能是实际需要的十几倍,网络传输和内存占用都浪费。改成显式列名,效果立竿见影,而且顺带解决了前面说的列序错位问题。
另一个抓手是减少往返次数。循环里逐条INSERT,一千条就是一千次网络往返,改成批量插入或者拼成一条多值语句,性能差一个数量级。ADO 本身没有特别优雅的批量方案,但可以用UpdateBatch配合客户端游标,或者干脆拼 SQL——前提是值全部经过严格校验或参数化,别为了性能把注入风险放进来。
提示:调优前先建基线。记下改造前的 P50、P99 耗时和错误率,改完再测一遍。没有基线就没有说服力,也容易出现过拟合式的"优化"。
7. 什么时候该换,什么时候该留
这个问题我被问过很多次,答案不是"越新越好"。
该换的信号有三个。一是系统还在持续迭代,新功能不断加,而 ADO 的写法让每次改动都提心吊胆,改造成本已经超过维护成本。二是并发量上来了,连接池管理、异步化、重试这些现代框架标配的能力,在 ADO 上全得手写,而且写不好就出事。三是团队里已经没人熟悉这套东西,知识断层比技术债更可怕。
该留的情况也很明确。系统进入维护期,需求变更很少,跑得稳,那改造的收益远小于风险。场景本身很轻,就是读个表、导个数据,上一整套数据访问框架纯属杀鸡用牛刀。或者受限于宿主环境,只能在 VBA、传统 ASP 里写,那 ADO 就是当下最合适的工具。
中间路线也值得考虑:不整体替换,而是在边界上做隔离。把数据访问收敛到一个薄层里,上层业务只管调方法,下层实现先用 ADO,将来要换再换实现。这个改造量小、风险可控,而且立刻能带来收益——SQL 集中了,参数化规范了,错误处理统一了,排查问题的时候不用满屏找conn.Execute。
我自己在实际操作中的体会是,技术选型的对错往往不在技术本身,而在于你有没有把它的边界摸清楚。ADO 的边界很清楚:它是个同步的、基于 COM 的、面向连接的访问层,擅长简单直接的取数写数,不擅长高并发、异步、复杂对象映射。你在它的舒适区里用它,它很可靠;你硬要它干不擅长的事,它就会用各种诡异的方式提醒你。摸清这条线,比记住多少个参数常量有用得多。
最后分享一个排查小技巧:遇到莫名其妙的 ADO 行为,先把conn.Provider、conn.ConnectionString(脱敏后)、rs.CursorType、rs.LockType、rs.CursorLocation这五个值打进日志。绝大多数"玄学问题"在这五个值面前都能现出原形,因为它们决定了整条链路的行为模式,剩下的事情只是顺着查下去。