
简介一份面向C开发者的ADO数据库访问练习项目主要解决在Visual C环境下通过ADO接口连接Oracle数据库、执行增删改查与结果集处理的问题。资源将常用数据库操作封装为CAdoDatabase类涵盖连接管理、SQL命令执行、记录集遍历、事务控制及错误捕获等典型场景适合刚接触数据库编程的读者学习参考。压缩包共40个文件以12个头文件、8个C源文件为主体辅以工程配置、资源脚本和说明文档同时包含msado15.tlh/tli类型库文件便于编译与调试。整个包仅209KB结构紧凑、分类清楚便于按模块阅读。目前已有130人学习。通过研读其中的类封装与调用示例可快速掌握ADO连接Oracle的步骤和常见排错思路也能直接借鉴到实际项目的数据访问层设计中。1. CAdoDatabase 到底多能打C 连 Oracle 从三天缩到半小时如果你的项目是在 Visual Studio 下用 C 开发数据库又是 Oracle那么 CAdoDatabase 这个类可能就是你正在找的后悔药。它把 ADO 的 Connection、Recordset、Command 三个对象封装成一组简单的 C 方法你在代码里不再需要跟 _ConnectionPtr 的 HRESULT 反复搏斗几条 SQL 就能跑起来。很多人用 OCCI 写了一个星期还没调通环境换这份封装后半小时就跑出了 SELECT 结果。这份 CAdoDatabase.rar 压缩包对应的就是这类封装资源头文件和实现文件都很精简适合 MFC、ATL 或者控制台工程Oracle 和 SQL Server 都能用。新手拿它当黑匣子也能跑通老手则可以直接拆开改成自己的数据访问层。下面我把拆过的类结构、接入步骤和常见坑一次说清。2. 类结构拆解CAdoDatabase 的 Connection 与 Recordset 封装逻辑2.1 三个核心对象Connection、Recordset、CommandADO 编程模型里程序员绕不开三个 COM 对象Connection 负责建立连接、事务和错误信息Recordset 负责缓冲查询结果Command 负责准备参数化 SQL 或存储过程调用。CAdoDatabase 做的事就是把它们包进一个普通 C 类让 MFC 程序员不直接面对 COM 接口。拆开这台“黑匣子”头文件核心声明大致是这个样子class CAdoDatabase { public: CAdoDatabase(); virtual ~CAdoDatabase(); // 连接与关闭 BOOL InitConnection(const CString strConn); void CloseConnection(); // 执行SQLINSERT / UPDATE / DELETE BOOL ExecuteSQL(const CString strSQL); // 执行SELECT查询 _RecordsetPtr OpenRecordset(const CString strSQL); // 事务操作 BOOL BeginTrans(); BOOL CommitTrans(); BOOL RollbackTrans(); protected: _ConnectionPtr m_pConnection; // ADO连接对象 CString m_strConn; // 保存连接串用于重连 };几个设计点值得注意。m_pConnection 是 _ConnectionPtr属于 COM 智能指针离开作用域会自动 Release所以析构函数里只需要 CloseConnection不需要手动释放 m_pConnection。m_strConn 保存最近一次成功的连接串目的是在监听断开后能重建连接。ExecuteSQL 和 OpenRecordset 分工明确前者执行增删改后者返回记录集正好对应 ADO 自身的 Command 与 Recordset 关系。到实际实现里OpenRecordset 通常要先检查 m_pConnection 的状态。如果 State 是 adStateClosed就用 m_strConn 重连否则进程运行久了连接被 Oracle 服务端断开第一次查询就直接崩溃。这个细节在后面避坑章节还会展开但你在第一次用这个类时就应该意识到它的价值。2.2 选型理由ADO 对比 OCCI 与 Pro*C这个类为什么值得下因为它把技术栈押在了 ADO 上。Oracle 场景里的三个主流方案我用下表做了对比方案外部依赖学习曲线跨数据库能力ADO CAdoDatabase只要 Oracle 客户端和系统自带 ADO浅封装后类库级别强Oracle/SQL Server/Access 可切换OCCI要 Oracle Instant Client OCCI SDK陡对象模型和迭代器复杂弱代码主要绑死 OraclePro*C要 Pro*C 预编译器和数据库头文件中SQL 混在 C 代码里弱基本只绑 Oracle放在具体项目里这个对比最重要的结论是如果今天连 Oracle明天可能改连 SQL ServerADO 是少数不用改业务代码的方案。CAdoDatabase 的初始化只是替换一个连接串查询、事务写法几乎原封不动。OCCI 对 Oracle 新特性支持确实更彻底比如对象类型和高级队列。但大多数数据库操作无非是增删改查加存储过程ADO 覆盖这些场景绰绰有余。我见过不少项目因为 OCCI 的动态绑定太麻烦最终绕回 ADO。CAdoDatabase 不是性能天花板却是交付速度的实用选择。尤其老项目已有 MFC 界面框架时引入 ADO 的成本比引 OCCI SDK 低一个量级。2.3 封装边界它不擅长什么使用之前要清楚边界。CAdoDatabase 不适合做批量游标也不适合大规模数据流处理。如果要一次性拉十几万行做报表_RecordsetPtr 默认前向游标仍会把数据缓存到客户端内存占用会很不好看。我的做法是数据量超过一万行就采用分页 SQL而不是让封装类承担大结果集。Oracle 12c 后的分页写法是 FETCH FIRST11g 及以下用三层 ROWNUM 嵌套。边界还体现在错误处理上。CAdoDatabase 的常见实现里ExecuteSQL 只返回 BOOL真正的错误细节被 COM 异常吞掉了你在调试时能看到 HRESULT但从界面看不到原因。所以强烈建议在类内部保留一个成员变量 m_strLastError每次调用失败后把 Error 对象里的 Description 取出来。这是这类封装最值得改的地方第 6 章做线程安全版本时会一并处理。判断错误时用下列代码就能拿到可读信息CString CAdoDatabase::GetLastError() { CString strErr; try { _com_error err(_com_error::GetErrorInfo()); strErr err.Description(); } catch (...) { strErr _T(未知ADO错误); } return strErr; }有了 GetLastError后面排查 Provider、监听、字符集问题会快非常多。这也是我在第 3 章就不断提错误信息的原因。3. 编译接入把 CAdoDatabase 挂进 MFC 工程的三步操作3.1 导入 ADO 类型库与必须的预处理MFC 工程里用 CAdoDatabase第一步不是 include 头文件而是让编译器认识 ADO 的 COM 接口。通常是在 stdafx.h 最后加入这段#import C:\Program Files\Common Files\System\ado\msado15.dll \ no_namespace \ rename(EOF, adoEOF) \ rename(BOF, adoBOF)no_namespace 表示不引入 msado15 的命名空间代码里可以直接写 _RecordsetPtr、_ConnectionPtr。rename 把 EOF 和 BOF 改名原因是 C/C 标准库已经定义了 EOF 宏不改就会编译冲突。如果装的是新版 Windows也可以写 #import msado15.dll 让编译器去环境变量路径找。我习惯写绝对路径避免不同机器上 VS 的 include 路径不一致。这段代码放 stdafx.h 末尾后还要保证工程链接了 ole32.lib。MFC 默认会链接但 ATL 工程要在项目属性-链接器-输入-附加依赖项里补上。因为 CoInitialize、CoCreateInstance 这些 COM 基础函数都在 ole32.lib 里。另一个容易翻车的是字符集。CAdoDatabase 内部用 CString工程如果是多字节字符集连接串里的字段名或中文路径会出现乱码。Visual Studio 新工程默认继承 Unicode老工程建议保持原设置但连接串统一用 _T 宏不要混用 char 和 wchar_t。3.2 初始化 COM 并构造连接串CAdoDatabase 的 InitConnection 常见写法是在内部调用 CoInitialize。你要注意是否重复初始化COM 允许重复调用但必须配对 CoUninitialize。如果在 App::InitInstance 里已经做了 OLE 初始化类内部的 CoInitialize 可以省略但保留也无妨。下面是一段可用的实现BOOL CAdoDatabase::InitConnection(const CString strConn) { ::CoInitialize(NULL); if (m_pConnection ! NULL) { m_pConnection.Release(); m_pConnection NULL; } HRESULT hr m_pConnection.CreateInstance(ADODB.Connection); if (FAILED(hr)) { m_strLastError _T(创建ADODB.Connection失败); return FALSE; } m_pConnection-ConnectionTimeout 10; // 建连超时秒数 m_pConnection-CommandTimeout 30; // SQL执行超时秒数 hr m_pConnection-Open(_bstr_t(strConn), , , adModeUnknown); if (FAILED(hr)) { m_strLastError _com_error(hr).ErrorMessage(); return FALSE; } m_strConn strConn; return TRUE; }三个参数值得调。ConnectionTimeout 控制 TCP 建连与 Oracle 监听握手内网可以调到 5 秒外网要放宽到 15 秒。CommandTimeout 控制单条 SQL 执行上限报表场景可以放大到 120 秒。Open 最后一个参数 adModeUnknown 表示连接模式未指定你要独占连接可以改成 adModeReadWrite。然后写 Oracle 连接串最常用的是这种CString strConn; strConn _T(ProviderOraOLEDB.Oracle.1;Data SourceORCL;) _T(User IDscott;Passwordtiger;Persist Security InfoFalse;); CAdoDatabase db; if (!db.InitConnection(strConn)) { AfxMessageBox(db.GetLastError()); return FALSE; }参数说明如下表连接串参数作用典型值Provider指定 OLE DB 提供程序OraOLEDB.Oracle.1Data SourceOracle 网络服务名或 EZCONNECTORCL或 //10.0.0.5:1521/orclUser ID登录用户名scottPassword登录口令tigerPersist Security Info是否在内存中保留密码False提示连接串里的 Data Source 建议先写网络服务名确认 tnsping 通后再换成 EZCONNECT 形式。这样区分错误阶段会很快。这里最大的坑是 Provider 写成 MSDAORA。MSDAORA 是微软自带的 Oracle OLE DB 提供程序对 Oracle 10g 之后的版本支持不好经常报“Oracle 客户端错误”。OraOLEDB.Oracle.1 是 Oracle 官方提供程序安装 Complete 客户端或 Instant Client 都会带。两个 Provider 的游标行为也有差异后面避坑专门讲。3.3 编译错误的三处经典拦路虎第一平台位数必须和 Oracle 客户端一致。Win32 程序就装 32 位 Oracle Clientx64 程序装 64 位。混装时 Provider 在注册表里也能看到但运行时提示“没有注册类”或“接口不支持”。检查方法是把 Oracle 安装目录的 bin 加入 PATH再用一条简单的连接测试看是否报错。第二头文件包含顺序。CAdoDatabase.h 依赖 CString必须在包含它之前处理过 afxwin.h 或 stdafx.h。如果你在纯控制台工程里直接包含会报“未定义的 CString”。控制台工程需要先包含 afx.h并把项目设置改为“在静态库中使用 MFC”。第三VS2015 以后编译器的 /permissive- 开关。老 CAdoDatabase 代码里经常把 _bstr_t 隐式赋值给 CString严格一致性模式下会报错。项目属性-C/C-语言-一致性模式改成“否”可以让老代码平滑编译。这不算推荐做法但项目排期紧时比重构代码快得多。编译通过后我建议先跑一条最简单的 SELECT 1 FROM DUAL确认连接串和 Provider 都没问题再进入第 4 章的实战操作。这一步能过滤掉大半环境问题。4. 实战操作查询、事务与 Oracle 存储过程调用4.1 查询结果集遍历_RecordsetPtr 的正确用法连接成功后最常见的操作是 SELECT。CAdoDatabase 里 OpenRecordset 返回 _RecordsetPtr遍历代码可以照抄_RecordsetPtr rs db.OpenRecordset(_T(SELECT empno, ename, sal FROM emp)); if (rs NULL || rs-adoEOF) { // 无数据 return; } while (!rs-adoEOF) { long nEmpNo rs-Fields-GetItem(_T(EMPNO))-Value; CString strEname (LPCTSTR)(_bstr_t)rs-Fields-GetItem(_T(ENAME))-Value; double dSal rs-Fields-GetItem(_T(SAL))-Value; TRACE(_T(%ld, %s, %.2f\n), nEmpNo, strEname, dSal); rs-MoveNext(); } rs-Close();两个细节需要养成习惯。GetItem 的字段名建议大写Oracle 返回的列名默认大写小写在部分版本会匹配失败。读取字段值时如果数据库字段是 NULLValue 是 VT_NULL直接赋给 long 或 CString 会抛异常。安全写法是先取 _variant_t 判断 v.vt VT_NULL或者用 Fields-GetItem(...)-GetStatus() 检查。打开记录集时最好指定游标类型。默认 adOpenForwardOnly 只能向前移动需要翻页或统计行数就得重新查询。CAdoDatabase 的 OpenRecordset 如果没暴露游标参数可以自己在类里加一个默认参数 adOpenKeyset。我一般直接让 OpenRecordset 默认打开键集游标避免二次踩坑。Oracle 分页在这里也要特别注意。11g 及以下用三层 ROWNUM 嵌套12c 以上可以直接 FETCH FIRST。示例// Oracle 12c CString strSQL _T(SELECT * FROM emp ORDER BY empno OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY); // Oracle 11g 及以下 CString strSQL11 _T(SELECT * FROM (SELECT a.*, ROWNUM rn FROM (SELECT * FROM emp ORDER BY empno) a WHERE ROWNUM 30) WHERE rn 20);不要把 SQL Server 的 TOP 语法搬到 Oracle 上ADO 不负责翻译方言。如果你用 CAdoDatabase 做大量 INSERT不要每次循环都 ExecuteSQL 一条。ADO 的 Connection 在同一个事务中执行速度取决于 Oracle 网络往返。通用提速做法是把 100 条拼成一条 INSERT ALL ... SELECT * FROM DUAL 语法或改成批量绑定但这个类没有封装需要自己用 Command 参数化来做。4.2 参数化命令与 Oracle 存储过程调用直接拼接 SQL 在 Oracle 下会遇到单引号转义、日期格式问题。参数化是更稳的做法。用 Command 对象调用存储过程完整代码如下_CommandPtr cmd; cmd.CreateInstance(ADODB.Command); cmd-ActiveConnection db.GetConnection(); cmd-CommandText _T({CALL get_emp_by_dept(?,?)}); cmd-CommandType adCmdText; // 输入参数 _ParameterPtr pIn cmd-CreateParameter(_T(p_deptno), adInteger, adParamInput, -1, long(20)); cmd-Parameters-Append(pIn); // 输出参数 _ParameterPtr pOut cmd-CreateParameter(_T(p_count), adInteger, adParamOutput, -1, 0L); cmd-Parameters-Append(pOut); cmd-Execute(NULL, NULL, adCmdText); long nCount pOut-Value;注意 Oracle 存储过程在 ADO 里要用花括号语法 {CALL proc(?,?)}。第二个问号是输出参数CreateParameter 第三个参数要传 adParamOutput。输出参数在 Execute 完成后从 pOut-Value 取而且长度参数不要传 -1部分驱动会报“参数类型定义冲突”。如果传中文字符串参数类型要写成 adVarChar长度必须大于等于存储过程入参定义的长度_ParameterPtr pName cmd-CreateParameter(_T(p_name), adVarChar, adParamInput, 100, _bstr_t(_T(会计部)));这里的 100 小于 varchar2(100) 时会触发 ORA-06502 字符串缓冲区不足报错信息直接指向参数长度。我踩过一次后凡是传字符串参数都会先把字段长度查出来再定义。4.3 事务批量写入的提交与回滚CAdoDatabase 封装了 BeginTrans、CommitTrans、RollbackTrans。事务用于多表一致性更新if (!db.BeginTrans()) { return; } try { db.ExecuteSQL(_T(UPDATE emp SET sal sal * 1.1 WHERE deptno 20)); db.ExecuteSQL(_T(INSERT INTO emp_log(deptno, action) VALUES(20, RAISE))); db.CommitTrans(); } catch (...) { db.RollbackTrans(); }用 ADO 事务要记住一条 Oracle 特性Oracle 默认自动提交但在 ADO 的 Connection 上开启 BeginTrans 之后到 CommitTrans 之前的 SQL 都在同一事务里。如果事务中调用了存储过程而存储过程内部有 COMMIT事务边界就被打断上面代码里的 RollbackTrans 可能只回滚一半。这是 Oracle 与 SQL Server 行为差异最大的地方。另一个细节BeginTrans 完成后连接不能断。如果 Oracle 空闲连接超时把连接断开CommitTrans 会失败并抛 COM 异常。事务方法内部先检查 m_pConnection-State 是否等于 adStateOpen断开就直接返回 FALSE记录“连接已断开事务未执行”。查状态比盲目调用 Commit 稳。还有一点是关于事务超时。ConnectionTimeout 只管建立连接CommandTimeout 只管单条命令事务整体时长需要自己在代码里计时。超过两分钟的长事务在 RAC 环境下容易触发全局死锁建议在循环中分批提交每批 500 行既减少 UNDO 压力也让来回重试的范围变小。5. 避坑指南五个 CAdoDatabase 连接 Oracle 的典型翻车现场先说一句这一章是我用这份资源最常见的血泪经验。很多问题表面是 CAdoDatabase 报错实际是 Provider 或 Oracle 客户端的配置错误。所以每一条我都按“现象→原因→解决”拆开方便你对照排查。5.1 COM 初始化顺序引发“未指定的错误”现象InitConnection 里 CreateInstance 成功Open 时抛异常GetLastError 返回空串调试看 hr 是 0x80004005 “未指定的错误”。在单步跟踪时程序会停在 Open 这一行也不进 catch直接弹出一个 COM 异常窗口。如果你把异常继续抛上去最终就是一句用户看不懂的“未指定的错误”。原因当前线程没有初始化 COM 公寓或者调用 InitConnection 的时机早于 AfxOleInit。CAdoDatabase 虽然内部有 CoInitialize但如果你在窗口类构造函数里建连接构造函数可能先于 App::InitInstance 的 OLE 初始化执行。MFC 里 AfxOleInit 是在 InitInstance 里被调用的而子窗口的构造函数常常出现在框架类成员初始化列表中顺序上很容易踩雷。解决把所有数据库操作放到按钮响应或工作线程中确保 InitConnection 之前先 ::CoInitialize(NULL)或者在 App::InitInstance 里调用 AfxOleInit()。我建议统一在 App 初始化时做一次 OLE 初始化CAdoDatabase 内部不再重复调用 CoInitialize避免忘记配对的释放。判断是否已初始化可以在 InitConnection 里用 CoInitialize(NULL) 的返回值如果返回 S_FALSE 说明已经初始化过不要立刻调 CoUninitialize。5.2 Provider 未注册还是 64/32 位混了现象开发机正常部署到服务器后报“未指定的错误”或“Provider cannot be found”。用 regsvr32 手工注册 OraOLEDB.dll 又提示模块路径错误或者注册成功但仍然找不到。更隐蔽的是在 64 位系统上32 位程序和 64 位 Oracle Client 的组合会报“Provider 未注册”而 64 位程序配 32 位 Client 也一样。原因OraOLEDB.Oracle.1 是 COM 组件32 位和 64 位组件在注册表里分属不同视图。程序是 x86服务器只装了 64 位 Oracle ClientProvider 注册到 64 位注册表位置32 位进程自然找不到。反过来的混搭也一样。解决先确认程序目标平台。x86 程序必须装 32 位 Oracle Clientx64 程序装 64 位。检查方式在 cmd 里执行 reg query HKLM\SOFTWARE\Classes\OraOLEDB.Oracle.164 位系统查 32 位项要带上 WOW6432Node。再把 Oracle Client 路径加入 PATH避免运行时加载不到 oraoledb 依赖的动态库。用 Process Explorer 查看进程加载的 DLL 路径可以确认是不是加载到了错误的版本。5.3 中文乱码NLS_LANG 与连接串字符集现象查询结果中文全是问号写入中文后再查是乱码。同一个 CAdoDatabase 在同事机器正常在自己机器乱。乱码的形态有几种一种是显示为 ??????说明是客户端字符集转换后丢失另一种是显示为锟斤拷说明字符集错乱后被按另一个编码解释。后者通常发生在 AL32UTF8 和 GBK 之间瞎设时。原因Oracle 客户端与服务器字符集不一致。NLS_LANG 被设成 SIMPLIFIED CHINESE_CHINA.ZHS16GBK数据库却是 AL32UTF8转换就会出错。如果你在连接串里写了 CharsetAL32UTF8但环境变量 NLS_LANG 是 GBKOraOLEDB 的行为也会变得很难预测。解决先用 SELECT * FROM V$NLS_PARAMETERS 查服务器字符集再设置环境变量 NLS_LANG比如 AMERICAN_AMERICA.AL32UTF8 或 SIMPLIFIED CHINESE_CHINA.ZHS16GBK必须与服务器一致。设置完重启进程。连接串里也可以加 Unicodetrue但对 OraOLEDB 来说NLS_LANG 的优先级更高。排查时可以打开注册表 HKEY_LOCAL_MACHINE\SOFTWARE\ORACLE\KEY_... 查看初始 NLS_LANG确认安装和覆盖的顺序。这是个典型的“换一台机器就翻车”问题排查时别只盯代码。5.4 空闲后查询报 ORA-03113连接被服务端强杀现象程序挂一晚上第二天第一次查询直接报 ORA-03113: end-of-file on communication channel。新建连接正常但长期运行的服务进程总是隔一段时间就掉一次日志里只有这句。有时候报的还会是 ORA-03114 未连接到 ORACLE但 ORA-03113 更常见。原因Oracle profile 的 IDLE_TIME 限制空闲会话服务端断开 TCP 后客户端不知道第一次操作才发现套接字已死。数据库管理员通常会在 resource_limit 开启后给应用账户设置 idle_time 为 30 或 60 分钟。你这边完全可能不知道这个策略因为开发环境不设限。解决在 OpenRecordset 里加重连逻辑如果 hr 是 0x80004005 且错误信息含 ORA-03113就释放旧连接用 m_strConn 重新 InitConnection再执行一次 SQL。也可以每 5 分钟执行 SELECT 1 FROM DUAL 保活但长期驻留不如出错重连干净。从架构上讲进程内缓存连接并在出错时重建比维护一条永不释放的连接更稳。重连函数要设置最大重试次数比如连续 5 次仍失败就返回错误避免服务端正在维护时客户端疯狂重连。5.5 Oracle 监听服务无法启动导致的假连接失败现象InitConnection 超时服务列表里 OracleOraDb11g_home1TNSListener 是停止状态手工启动立刻失败。有时监听已经启动但 tnsping ORCL 还是超时换 EZCONNECT 写法又能连上。这种问题很容易被误判成代码问题其实代码一行都不用改。原因listener.ora 配置与网络环境冲突。常见是 hosts 文件把机器名解析到 IPv6 ::1监听器启动时 bind 不到 0.0.0.0也可能 tnsnames.ora 里主机名解析不到。Oracle 11g 之后默认监听会试图绑定机器名对应的 IP如果 hosts 里有类似 “127.0.0.1 localhost” 又同时有 “::1 localhost”动态注册就会失败。解决先检查 listener.ora 的 HOST 是否写成实际内网 IP 或 localhost再用 lsnrctl status 查看监听状态用 tnsping ORCL 验证网络服务名。CAdoDatabase 连接串如果写成 Data SourceORCL就依赖 tnsnames.ora。想绕过 Oracle 网络服务名可以写成 Data Source//192.168.1.10:1521/orcl这样 tnsnames.ora 的错误就不会影响你快速联调。如果你在梳理 Oracle EBS 这类大系统监听和 EBS 的环境文件关联很深改主机名要非常谨慎建议先做快照再动。6. 进阶技巧把 CAdoDatabase 改造成线程安全的数据访问层线程安全最简单的方式是“永不共享连接”。CAdoDatabase 内部是 _ConnectionPtrCOM 智能指针可以在线程间传递但 Open 之后连接会绑定创建线程的公寓。MFC 多线程里一条连接在后台线程创建UI 线程使用轻则界面卡死重则 ADODB.Connection 直接拒绝调用。所以我一般给每个线程单独开连接而不是加锁复用。可以用线程 ID 到 CAdoDatabase 实例的映射表实现class CMyDBPool { public: CAdoDatabase* GetThreadDB() { CAdoDatabase* pDB nullptr; m_cs.Lock(); if (!m_map.Lookup(::GetCurrentThreadId(), pDB)) { pDB new CAdoDatabase(); pDB-InitConnection(m_strConn); m_map.SetAt(::GetCurrentThreadId(), pDB); } m_cs.Unlock(); return pDB; } void ReleaseThreadDB() { CAdoDatabase* pDB nullptr; m_cs.Lock(); if (m_map.Lookup(::GetCurrentThreadId(), pDB)) { pDB-CloseConnection(); delete pDB; m_map.RemoveKey(::GetCurrentThreadId()); } m_cs.Unlock(); } private: CMapDWORD, DWORD, CAdoDatabase*, CAdoDatabase* m_map; CString m_strConn; CCriticalSection m_cs; };代码逻辑很直接GetThreadDB 先查当前线程有没有实例没有就新建。每个线程独立连接查询期间没有锁竞争CCriticalSection 只保护 map 操作不包住 SQL 执行。数据库连接在使用时才创建自然形成按需连接配合前面的断线重连长跑项目也能稳定。这里有个陷阱map 存的是原始指针线程退出时必须调用 ReleaseThreadDB否则连接泄漏。更稳妥的方案是用 __declspec(thread) 的 TLS 变量存储连接指针线程销毁时自动释放。我在后续项目里把 InitConnection 的失败重试和 GetLastError 也全塞进了这个池类业务代码里再也看不到 CAdoDatabase 内部细节。从那以后我每次封装数据库都强制走一遍“连接串分离→最小查询→线程隔离→空闲重连”四步遇到连接丢失也很少再翻车。希望帮到你。本文还有配套的精品资源点击获取