ADO封装类实战:用C#安全驾驭Connection、Recordset与事务
发布时间:2026/9/26 21:26:34来源:尧图网络
简介这是一份面向C/C开发者的ADO数据库操作封装类适合在Visual C、MFC等桌面项目中快速接入SQL Server、Oracle或Access直接用于数据读写、参数化查询与事务处理。类库为2.0版本已对大部分常用数据库操作做了收敛开发者无需深入COM对象细节只需调用封装接口即可完成连接管理、记录集遍历、增删改查及错误处理。压缩包共2个文件由一个头文件与一个源文件组成整体仅15KB结构紧凑非常便于阅读、移植和按需扩展。目前已有181人学习适合刚接触数据库封装、希望降低数据访问层耦合度的开发者。通过源码可以了解ADO对象模型的实际用法、封装类接口设计以及运行异常的处理方式并可作为模板继续完善日志或批量操作等功能。1. 一个 ADO 封装类为什么现在还有人翻出来用维护老系统的人最怕两件事数据库驱动换版本和接手一份没人敢动的 ADO 代码。ADO 操作类也叫 ado 封装、数据库封装类就是把 Connection、Recordset、Command 这些 COM 对象的打开、查询、写库、事务收进一个类里。它解决的不是性能问题而是连接泄漏、游标错用、事务没提交这类“看起来能跑、半夜就挂”的维护问题。适合三类人还在维护 VB6/ASP 老项目的需要读 Access、Excel 的桌面工具开发以及想给团队留一把不依赖 EF 的轻量数据钥匙的人。下面这套 AdoClient 封装是我在几个遗留系统里反复用过、也踩过坑之后的版本能直接落到 Access、Excel 和 SQL Server 上。2. ADO 封装类要管的四件事连接、游标、命令与事务边界2.1 先分清 ADO 与 ADO.NET两种连接模型两套思维很多从 .NET 起步的同事第一次看到 ADO 代码容易把它当成 ADO.NET 的旧称这个误会很贵。ADO 是 COM 时代的数据库访问组件核心对象是 Connection、Command、Recordset它们活在进程内和数据库保持一条真实的活动连接。而 ADO.NET 是断开式模型SqlConnection 用完该关就关DataTable 是纯内存数据。两者代码长得有点像底层思维完全不同。用 ADO 写封装类首先要想清楚 Recordset 是这个类的灵魂。Recordset 不是 DataTable它更像一个“活着的游标”可以往前滚、往后滚、原地改值甚至可以批量更新回数据库。这个特性既是价值也是危险封装类设计得怎么样全看你怎么处理 Recordset 的游标类型、锁定类型和生命周期。对比项ADO本封装类操作的对象ADO.NET底层模型COM进程内活动连接托管断开式连接数据载体Recordset游标DataSet / DataTable连接释放必须显式 Close ReleaseComObjectusing Dispose断开后读数据依赖 CursorLocationadUseClient天然支持跨数据库方言SQL 字符串 Provider 差异需要第三方方言层这张表不是背概念它决定了封装类的取舍。比如你要做只读报表Recordset 就用静态游标加只读锁你要做批量回写就得换个游标。封装类把这两个开关做成默认参数调用方不用每次翻文档这就是“封装”的第一层价值。新手接手老项目时看到代码里到处 new Recordset、new Connection然后靠记性去 Close十有八九要漏。这不是人的问题是对象生命周期太分散需要有人统一收口。提示ADO 里的连接和 ADO.NET 里的连接不是一个东西。封装类内部管的是 ADODB.Connection 这个 COM 对象不要拿 SqlConnection 的思路直接套。2.2 封装类的职责边界六件事做进去四件事留在外面一个成熟的 ADO 封装类不管叫 AdoClient 还是叫 DbHelper核心职责就那么六件打开连接、关闭连接、执行查询返回 Recordset、执行写操作返回影响行数、开事务、提交或回滚事务。把这六件事做扎实类就值了。最常见的翻车是把业务 SQL 也写进类里比如类里直接出现 SELECT * FROM Orders 这种字符串表面是“复用”实际是给替换数据源、写单元测试埋了死结。我的习惯是连接字符串从外部配置传进来SQL 由调用方传参类只负责执行和释放。类里不放任何业务表名不搞实体映射不做“多数据库方言自动转换”。为什么不做方言转换因为 ADO 封装类本来就薄一旦引入方言层等于自己写一个迷你 ORM测试成本翻倍收益却不高。老系统迁移时替换 Provider 和连接串就够了SQL 里的分页写法、日期函数差异那是 SQL 层的问题不该由这个类背。类内做六件事类外做调用方负责Connect / Close连接串放配置不写死在类里Query 返回 RecordsetSQL 文本与参数值ExecuteNonQuery 返回行数业务逻辑与结果集处理BeginTrans / Commit / Rollback数据源方言与分页语法Connection 属性暴露给高级调用实体映射、日志埋点这个边界还有一个实际好处替换数据源时不用改类。同一个 AdoClientAccess 能用Excel 能用SQL Server 也能用。你只需要改连接串和少数 SQL 语法。给老系统做技术验证时先在 Access 上把逻辑跑通再换 SQL Server排错压力小很多。2.3 Provider 选型连接串的第一个词决定后面所有坑连接字符串的第一段永远是 Provider这词写错后面全是白费。Provider 决定了 ADO 用哪个驱动去访问数据源也决定了你接下来会遇到哪一类坑。常见的就这么几个Provider适用数据源现状Microsoft.Jet.OLEDB.4.0Access 97-2003 (.mdb)官方早已停止维护仅 32 位Microsoft.ACE.OLEDB.12.0Access 2007 (.accdb / .xls / .xlsx)最通用需装 AccessDatabaseEngineMicrosoft.ACE.OLEDB.16.0Office 2016 环境与 12.0 的 32/64 位版本不能共存SQLNCLI11SQL Server 2008Native Client 11稳定MSOLEDBSQLSQL Server 2012微软新一代 OLE DB Driver拿到一台新机器别急着写代码。先确认这台机器上到底注册了哪些 Provider再决定连接串怎么写。PowerShell 一条命令就能列出来# 列出本机已注册的 OLE DB Provider确认 ACE 或 SQLNCLI 是否存在 $provider New-Object System.Data.OleDb.OleDbEnumerator $provider.GetElements() | Where-Object { $_[SOURCES_NAME] -match OLEDB|SQLNCLI } | Format-Table SOURCES_NAME这段命令里OleDbEnumerator 是 .NET 自带的枚举器不需要额外装东西。看到 SOURCES_NAME 列表里没有 ACE.OLEDB.12.0就去装对应位数的 AccessDatabaseEngine看到 SQLNCLI11 缺失就去装 SQL Server Native Client。这个“先枚举再选型”的习惯能帮你把一半的现场问题挡在编码之前。注意Provider 的首选顺序不是按性能排的是按兼容性排的。老机器上 Jet 4.0 还在但新机器默认没有ACE 12.0 能同时覆盖 .mdb 和 .accdb比 Jet 稳得多。能上 ACE 就别留恋 Jet。3. 用 C# 实现 AdoClient 封装类每个方法的参数都说明白3.1 引用与初始化COM 引用的两种方式在 C# 里操作 ADO第一步是把 ADODB 引用进来。常见做法是在 Visual Studio 里右键引用选“添加 COM 引用”找到 Microsoft ActiveX Data Objects 6.0 Library这是最直接的方式生成的 Interop.ADODB.dll 会被自动放在输出目录部署时跟着走。如果你不想生成 Interop 程序集也可以用反射动态创建 COM 对象这样能避免 Interop 版本不匹配的问题但代码写起来很啰嗦对于封装类来说不划算。常规做法就是添加 COM 引用项目里少一个黑匣子调试时能直接看到 ADODB 的类型定义。建类的骨架using System; using System.Runtime.InteropServices; using ADODB; public class AdoClient : IDisposable { private Connection _conn; /// summary当前活动连接高级调用如批量提交可以直接拿它用/summary public Connection Connection _conn; }这段代码只做了两件事持有 Connection 引用并对外暴露一个只读属性。把 Connection 设为公开属性不是偷懒是为了让封装类保持“薄”的定位——有些操作比如客户端游标批量更新绕不开原始对象与其再加一层半吊子方法不如把连接对象交出去由高级调用方自行控制。对新手来说这个属性带来的自由度可能用不上但老系统维护时它是很多奇技淫巧的唯一出口。3.2 Connect 与 Close连接生命周期只由这两个方法负责连接管理是封装类最核心的部分也是出问题最多的部分。我的实现里Connect 负责幂等打开Close 负责彻底释放二者成对出现并且内部做了状态判断private readonly object _lock new object(); public void Connect(string connectionString, int timeoutSeconds 15) { lock (_lock) { if (_conn ! null _conn.State ObjectStateEnum.adStateOpen) return; _conn new Connection { ConnectionString connectionString, ConnectionTimeout timeoutSeconds, CursorLocation CursorLocationEnum.adUseClient }; _conn.Open(); } } public void Close() { lock (_lock) { if (_conn ! null) { if (_conn.State ObjectStateEnum.adStateOpen) _conn.Close(); Marshal.FinalReleaseComObject(_conn); _conn null; } } } public void Dispose() { Close(); GC.SuppressFinalize(this); }Connect 里的第一个判断是幂等入口已经打开就直接返回防止调用方重复 Open 造成连接数暴涨。CursorLocation 默认设成 adUseClient这是 Query 方法能安全断开读取的前置条件后面会细讲。Close 方法里最关键的是Marshal.FinalReleaseComObject(_conn)这一行是把 COM 引用计数彻底归零的后悔药。ADO 是 COM 组件靠引用计数管理生命周期光调 Close 不释放 COM 对象进程退出前连接常常不归还表现在现象上就是“程序内存一直涨、数据库连接数一直挂”。提示Close 和 Dispose 是两个层面的事。Close 关掉连接把连接置空Dispose 让封装类可以被 using 回收。两个方法成对出现谁也别省。3.3 Query 方法游标与锁的默认值要写对Query 负责把 SQL 变成可以安全读取的 Recordset。这里最容易犯的错是拿到 Recordset 不设置游标直接返回给调用方结果调用方一查 RecordCount 得到 -1或者连接一关数据就没了。正确写法是在 Open 之前就把游标和锁都定死public Recordset Query(string sql, params object[] values) { if (_conn null || _conn.State ! ObjectStateEnum.adStateOpen) throw new InvalidOperationException(连接未打开先调用 Connect。); var cmd BuildCommand(sql, values); var rs new Recordset { CursorLocation CursorLocationEnum.adUseClient }; rs.Open(cmd, null, CursorTypeEnum.adOpenStatic, LockTypeEnum.adLockReadOnly, (int)CommandTypeEnum.adCmdText); return rs; }Query 的逻辑说三件事。一是游标位置CursorLocation adUseClient必须在 Open 之前设置设晚了不起作用。客户端游标把数据拉到本地连接关闭后 Recordset 仍然可读这对桌面工具和报表场景非常友好。二是游标类型adOpenStatic让 RecordCount 可用结果集是快照不实时但稳定。三是锁定类型adLockReadOnly保证查询不碰锁避免读操作把写事务堵住。BuildCommand是参数化公共逻辑专门处理 SQL 参数private Command BuildCommand(string sql, object[] values) { var cmd new Command { ActiveConnection _conn, CommandType CommandTypeEnum.adCmdText, CommandText sql }; for (int i 0; i values.Length; i) { var p cmd.CreateParameter($p{i}, InferType(values[i]), ParameterDirectionEnum.adParamInput, InferSize(values[i]), values[i] ?? DBNull.Value); cmd.Parameters.Append(p); } return cmd; } private DataTypeEnum InferType(object v) { if (v null || v DBNull.Value) return DataTypeEnum.adVarChar; if (v is int) return DataTypeEnum.adInteger; if (v is long) return DataTypeEnum.adBigInt; if (v is decimal || v is double || v is float) return DataTypeEnum.adDouble; if (v is DateTime) return DataTypeEnum.adDBTimeStamp; if (v is bool) return DataTypeEnum.adBoolean; return DataTypeEnum.adVarWChar; } private int InferSize(object v) { if (v null || v DBNull.Value) return 255; if (v is string s) return Math.Max(1, s.Length); return 0; }这一段是封装类里最容易被忽略的部分。CreateParameter 的第四个参数是大小对定长类型传 0 没问题对字符串类型必须给足。取字符串实际长度是一种常见做法因为每个参数都对应一个新 Command不存在复用时长度不够的问题。null 值要单独处理给 null 时类型推断为 adVarChar、大小给 255否则 CreateParameter 会因为无法推断类型直接抛异常。3.4 ExecuteNonQuery参数化写库与影响行数写操作INSERT、UPDATE、DELETE和查询返回 Recordset 的路径不同要用 Command.Execute 并且不要结果集。ExecuteNonQuery 的返回值是影响行数调用方拿它判断这次操作到底有没有写进去public int ExecuteNonQuery(string sql, params object[] values) { if (_conn null || _conn.State ! ObjectStateEnum.adStateOpen) throw new InvalidOperationException(连接未打开先调用 Connect。); var cmd BuildCommand(sql, values); object recordsAffected null; object missing Type.Missing; Recordset rs cmd.Execute(out recordsAffected, ref missing, (int)ExecuteOptionEnum.adExecuteNoRecords); if (rs ! null) Marshal.FinalReleaseComObject(rs); return recordsAffected null ? 0 : Convert.ToInt32(recordsAffected); }adExecuteNoRecords这个选项的意思是“这条命令不会返回 Recordset”让 ADO 跳过结果集处理速度更快。注意 Execute 的第一个输出参数recordsAffected在命令是 UPDATE 时返回受影响行数在命令是 SELECT 时返回 -1。调用方如果拿到 -1就应该意识到 SQL 走错了分支。第二个参数是保留参数常规写法传Type.Missing如果你的 Interop 版本编译不过把这个参数声明成object missing Type.Missing;再按引用传进去即可。参数化写库的价值在于防注入。字符串拼接遇到单引号、中文引号都容易翻车参数化之后值是值SQL 是 SQL两者由驱动分开处理。老系统里最常见的修复动作就是把拼接 SQL 改成这个方法的参数化调用改完后基本不会再出现“输入一个单引号程序就炸”的线上事故。3.5 事务三件套BeginTrans / CommitTrans / RollbackTrans事务封装很简单三个方法对应 ADO 的原始调用public void BeginTrans() { EnsureOpen(); _conn.BeginTrans(); } public void CommitTrans() { EnsureOpen(); _conn.CommitTrans(); } public void RollbackTrans() { EnsureOpen(); _conn.RollbackTrans(); } private void EnsureOpen() { if (_conn null || _conn.State ! ObjectStateEnum.adStateOpen) throw new InvalidOperationException(连接未打开。); }这套封装看着无脑真正的门道在调用纪律。ADO 的 BeginTrans 在同一个连接上是不能嵌套的连续调用两次会抛 “不能在此事务中启动更多事务”。所以封装类不建议提供嵌套事务方法而是建议调用方在业务层把事务边界收敛到一个方法里。我写过的最稳的事务模板是外层 trycatch 里 RollbackTrans 并抛出finally 里判断有没有提交决定要不要补救。这个模板放到靠后的章节说但设计封装类时事务方法保持“一对一、无嵌套、无隐藏状态”是最安全的。事务和连接池的交互是个大坑。连接如果被池化了事务没提交就归还连接下一次从池里拿到连接的代码会继承上一个未提交事务的上下文轻则死锁重则数据被莫名回滚。所以封装类里事务方法必须和 Close 配合好要么在事务完成前不要 Close要么在事务异常时强制 RollbackTrans。这条路没有银弹只有纪律。4. 三种数据源的连接串与调用示例Access、Excel、SQL Server4.1 Access 2007 用 ACE 12.0Jet 4.0 只留给你两个选择Access 是 ADO 封装类最常见的服务对象尤其是老 ERP 的报表库。连接串写法固定ProviderMicrosoft.ACE.OLEDB.12.0;Data SourceD:\data\orders.accdb;这里只有两个可换的东西Provider 和 Data Source。Jet 4.0 还在服役的机器通常跑的是 .mdb 文件如果你必须支持那些机器Provider 改成 Microsoft.Jet.OLEDB.4.0 即可。但新项目就别再选 Jet 了ACE 12.0 同时兼容 .mdb 和 .accdb一个驱动顶两个。32 位和 64 位的问题后面专门讲这里先记住一条Access 驱动优先配 32 位程序Office 默认装 32 位的情况远多于 64 位。调用示例查一笔明细var db new AdoClient(); db.Connect(ProviderMicrosoft.ACE.OLEDB.12.0;Data SourceD:\\data\\orders.accdb;); // Access 的参数占位符是 ?不是 name Recordset rs db.Query(SELECT id, customer, amount FROM orders WHERE amount ?, 1000m); while (!rs.EOF) { Console.WriteLine(${rs.Fields[id].Value}|{rs.Fields[customer].Value}|{rs.Fields[amount].Value}); rs.MoveNext(); } rs.Close(); db.Close();这段代码里有两个 Access 特有的细节。第一个是?占位符Access 的 OLEDB 驱动不认name这种命名参数只认位置占位符参数值按顺序绑定。第二个是读取循环Recordset 的 EOF 一开始就可能是 true查询结果为空时所以必须进循环前判断否则直接访问 Fields 会抛“没有当前记录”。包装类解决不了这种数据为空的问题调用方要有这个意识。4.2 Excel 当数据库HDR、IMEX 与表名语法把 Excel 当数据库读是封装类另一个高频用途。连接串比 Access 长一点难点全在 Extended PropertiesProviderMicrosoft.ACE.OLEDB.12.0;Data SourceD:\data\report.xlsx;Extended PropertiesExcel 12.0 Xml;HDRYES;IMEX1;三个参数各管一件事。HDRYES 表示第一行是列名HDRNO 表示第一行是数据列名自动变成 F1、F2。IMEX1 表示混合类型列按文本读取避免“这一行是数字、下一行是字符串”时驱动把整列推断成某一种类型导致其他行读成 null。表名要写成[Sheet1$]这种带方括号和美元符号的格式Sheet 名不对会直接报“Microsoft ACE OLE DB 找不到工作表”。读取前三行验证列名是现场检查最常用的做法Recordset rs db.Query(SELECT TOP 3 * FROM [Sheet1$]); for (int i 0; i rs.Fields.Count; i) Console.Write(rs.Fields[i].Name \t); Console.WriteLine(); rs.Close();这一步输出的列名如果和表头对不上先不要怀疑封装类大概率是 HDR 设错了。IMEX1 不是万能的Excel OLEDB 驱动对列类型的推断本身有点玄学同一个列里前几行是数字后面是文本它可能只把该列当 double文本行读出来是 null。遇到这种数据常规做法是改数据源格式或者把 IMEX 去掉让驱动按真实类型推断再针对性转类型。4.3 SQL Server 用 SQLNCLI11超时与加密参数的取舍SQL Server 的连接串Provider 直接用 SQLNCLI11这是 SQL Server Native Client 11比老的 SQLOLEDB 快也支持更多数据类型。本地开发推荐加Encryptno和TrustServerCertificateyes否则新版驱动默认加密连接本地开发经常报证书错误ProviderSQLNCLI11;Data Source.;Initial Catalogmydb;User IDsa;Password你的密码;Encryptno;TrustServerCertificateyes;参数说明Data Source 可以是localhost、.或服务器名\实例名Initial Catalog 是数据库名User ID 和 Password 对应 SQL 登录Windows 集成认证要把这两个换成Integrated SecuritySSPI。Encryptno只在开发和内网环境用生产环境建议改成Encryptyes并配置好证书。带参数的 UPDATE 是实际场景中最常见的写操作封装类执行方式如下int affected db.ExecuteNonQuery( UPDATE orders SET status p0, remark p1 WHERE id p2, 已发货, 快递单号待补, 1024); Console.WriteLine($影响行数{affected});这里注意参数顺序和 SQL 里p0、p1、p2的对应关系。SQL Server 认命名参数所以 SQL 里必须写p0这种名字和 CreateParameter 时的p{i}一一对应。SQLNCLI11 支持批量参数和流式读取但封装类默认不开启这些特性保持最简单的调用路径等真有性能瓶颈再在调用方做扩展。注意Command 的超时时间和连接超时时间是两回事。连接超时是建连时等的秒数默认 15 秒命令超时是查询执行时等的秒数ADO 默认 30 秒。长报表查询经常死在命令超时上封装类里预留一个 Timeout 参数调用方传 0 表示不限制。5. ADO 封装类避坑清单五个让老手翻车的细节5.1 RecordCount 永远 -1游标类型背锅现象SELECT 查出来明明有 200 行rs.RecordCount返回 -1翻页计数全乱。原因Recordset 默认的游标类型是 adOpenForwardOnly只能从前往后扫驱动不知道总行数RecordCount 自然返回 -1。这不是 Bug是游标类型的特性。想要总行数必须用静态游标或键集游标。解决Query 方法里已经固定了adOpenStatic这个坑主要是绕过封装类、自己 new Recordset 的人踩的。如果在自己代码里建 RecordsetOpen 前显式写var rs new Recordset { CursorLocation CursorLocationEnum.adUseClient }; rs.Open(SELECT ..., conn, CursorTypeEnum.adOpenStatic, LockTypeEnum.adLockReadOnly, 1);记忆点RecordCount 是不是 -1在 Open 之前由 CursorType 决定CursorLocation 决定的是连接关闭后还能不能读。两个开关互相独立别搞混。5.2 adUseClient 下 Update 不写库现象把 CursorLocation 设成 adUseClient 后改 Recordset 里的字段值调rs.Update()不报错但数据库里的数据纹丝不动。原因客户端游标的数据是拉到本地内存里的快照。修改快照里的值只是本地动作要真正写回数据库需要锁定类型支持批量更新并且调用UpdateBatch()。而通常配合的adLockReadOnly只读锁不会写库adLockOptimistic在客户端快照上也没有可靠的回写通道。解决写回场景不要用客户端游标直接用 ExecuteNonQuery 发 UPDATE 语句简单明确。这个坑是封装类设计时就要规避的——我的 Query 方法默认返回只读客户端快照天然不许写调用方想改数据就去走 ExecuteNonQuery从机制上把两种场景隔离。5.3 事务忘提交连接池把脏状态带回下一次调用现象程序没有报错但数据库里莫名多了一堆未提交事务后面再执行查询时有时卡死有时数据被回滚像是玄学。原因BeginTrans 之后代码抛了异常CommitTrans 没执行连接归还给连接池时事务还挂着。下一次从池里拿到这条连接的代码操作的事务上下文是脏的轻则锁表重则把别人的提交一起回滚。解决事务必须写在 try/catch/finally 三件套里finally 里只做一件事——判断事务是否还需要收尾需要就 rollback。给出我常用的模板bool committed false; try { db.BeginTrans(); db.ExecuteNonQuery(UPDATE ..., ...); db.ExecuteNonQuery(INSERT ..., ...); db.CommitTrans(); committed true; } catch { if (!committed) db.RollbackTrans(); throw; }这个模板看着简单它保证所有异常路径都不会把未提交事务带回连接池。老系统改造时我一般先把所有裸 BeginTrans 改成这个模板改完再谈别的优化。5.4 ACE 驱动 32/64 位不匹配现场最常见的“未注册”现象开发机跑得好好的部署到客户机器上一连接就报“未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0 提供程序”。原因AccessDatabaseEngine 分 32 位和 64 位两个版本一台机器只能装一个12.0 的 32 位和 64 位不能共存。而 Visual Studio 编译出来的程序如果是 AnyCPU 或 x64在 64 位操作系统上会以 64 位进程运行它找不到 32 位的 ACE 驱动反之亦然。客户机器上装的 Office 是 32 位绝大多数情况驱动也随 Office 装成 32 位程序却是 64 位直接翻车。解决项目平台目标统一锁成 x86跟随 Office 的位数。路径在项目属性 - 生成 - 平台目标选 x86别用 AnyCPU。再配合部署清单写清楚目标机器需要装对应位数的 AccessDatabaseEngine。这个检查我基本每次进现场都做问三句话Office 是几位、程序是几位、AccessDatabaseEngine 装了没有。三句话答完问题定位一半。5.5 NULL、EOF 与参数化占位符三个取值前的细节现象rs.Fields[remark].Value返回 DBNull直接ToString()能跑但(string)强转抛异常空结果集时访问 Fields 直接报“没有当前记录”Access 上参数写name报语法错误。原因ADO 字段值是 COM 的 Variant在 C# 里空值表现为 DBNull.Value不是 null。EOF 为 true 时 Recordset 没有当前记录Fields 集合是空的。占位符则是数据源差异Access 认?不认nameSQL Server 认name不认?。解决读取字段前先判断 EOF取字段值先判断 DBNull 再转换。写一个小的辅助扩展方法省得每次都要写两行判断public static string GetString(Recordset rs, string fieldName) { if (rs null || rs.EOF) return null; object val rs.Fields[fieldName].Value; return val DBNull.Value ? null : Convert.ToString(val); }占位符问题属于 SQL 层差异封装类不做转换调用方写 SQL 前先确认数据源。Access 一律用?SQL Server 一律用p0风格。这两个规则记牢取值相关的坑能少一大半。6. 进阶用法批量提交与连接泄漏自检6.1 UpdateBatch 批量写库客户端记录集要这样建逐行 INSERT 面对几千行数据时性能很难看ADO 的客户端记录集配合 UpdateBatch 可以一次性提交。做法是手动构造一个客户端 Recordset定义字段、AddNew、赋值最后挂到连接上批量提交var rs new Recordset { CursorLocation CursorLocationEnum.adUseClient, ActiveConnection db.Connection }; rs.Fields.Append(id, DataTypeEnum.adInteger, 0); rs.Fields.Append(name, DataTypeEnum.adVarWChar, 50); rs.Open(); for (int i 0; i 1000; i) { rs.AddNew(); rs.Fields[id].Value i; rs.Fields[name].Value $user_{i}; } rs.UpdateBatch(); rs.Close();这段代码的关键在第三行ActiveConnection 必须在 Open 之前设置。UpdateBatch 是把客户端本地记录集的变更一次性发给连接没有连接它不知道往哪写。实际工作中这个方法的性能比逐行 INSERT 高一个量级但要记住它是快照式提交提交前发现某行数据有问题只能整体回滚不能只撤一行。6.2 连开 5000 次连接验证封装类没有泄漏封装类的可靠性要用数据说话我每次改完连接代码都会做一个压力自测循环开连接、查询、关闭重复 5000 次同时打开任务管理器观察进程的内存曲线。内存曲线平稳说明 COM 引用释放干净曲线一路爬升说明有 RCW 没释放需要复查 Marshal.FinalReleaseComObject 的调用路径。for (int i 0; i 5000; i) { using (var db new AdoClient()) { db.Connect(ProviderMicrosoft.ACE.OLEDB.12.0;Data SourceD:\\data\\orders.accdb;); Recordset rs db.Query(SELECT COUNT(*) FROM orders); rs.Close(); } }这个循环跑完内存涨幅不超过几十 MB 就算合格。如果发现内存持续上涨先查是不是有 Recordset 没释放再看是不是连接对象被反复 new 却没有 FinalReleaseComObject。这套自测做顺手之后几乎每次都能在测试阶段把泄漏问题截住而不是等运维半夜打电话来。我养成的习惯是所有 COM 对象用完统一 Close 加 FinalReleaseComObject绝不让 GC 帮我兜底事务一定写在 try/catch/finally 里finally 里只看一个标记决定提交还是回滚。这套纪律让我少接了很多半夜电话希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网