简介:AdoDB.rar是一份面向C++开发者的数据库操作源代码包,基于微软ADODB组件(ActiveX Data Objects)实现与SQL Server、Oracle、MySQL等数据库的交互,重点展示Connection、Command、Recordset三大对象的使用方法。包内包含完整工程文件,通过ADODatabase.h与ADORecordset.h两个核心头文件,演示了建立连接、执行SQL命令、遍历结果集、增删改记录以及游标与锁定策略等典型场景。
资源共79个文件,以.cpp源文件、.h头文件为主,附带.dll/.lib库文件、.obj编译中间文件、.pdb调试信息和.sln解决方案等,整体38.43MB,适合中途接手或需要快速了解ADODB封装结构的开发者直接参考。工程同时提供VS2013工程文件,便于在对应环境下编译调试。
已有111人浏览学习,适合正在学习C++数据库编程或需要封装ADODB接口的读者,可从源码中提炼连接管理、异常捕获、类型库注册(如regtlibv12)等关键细节,缩短自研数据库访问模块的排错路径。
1. 为什么 C++ 数据库操作绕不开 AdoDB:一套封装好的连接、记录集与错误处理源码
当regtlibv12还躺在你的批处理脚本里,说明你正走在 C++ 和数据库打交道的老路上:用 ADODB 访问 SQL Server、Oracle 甚至 MySQL。做 C++ 数据库开发的人基本都碰过 ADODB,但真到了_ConnectionPtr、_RecordsetPtr满天飞的阶段,新手很容易被 COM 接口的生命周期绊倒。这套 AdoDB.rar 不是教学文档,而是一个可以直接编译的 VS2013 工程,压缩包内把 ADODatabase 连接封装、ADORecordset 记录集封装、ADOCommand 命令执行和 DBError 错误处理拆成独立模块,连 sigslot.h 信号槽都准备好了。它解决的最大问题是:你不需要从零啃 OLE DB 的繁琐初始化,只要照着 ADODatabase.h 和 ADORecordset.h 两个头文件,就能把「建立连接 → 执行 SQL → 遍历结果 → 处理错误」这条链路完整跑通。适合想快速在自己的 C++ 项目里接入数据库读写,又不想被 COM 细节拖住的人。后面我会顺着源码结构拆开讲,重点放在哪些能直接抄走、哪些配置不改就会翻车。
2. 拆 AdoDB 源码包:ADODatabase 与 ADORecordset 的封装层次
拿到压缩包先把文件结构过一遍,你会发现它并不是把 ADO 的 COM 接口平铺出来,而是按照「连接、命令、记录集、错误」四个职责切成了独立模块。这种分法在工程上是对的:业务代码只和 C++ 对象打交道,不直接碰_ConnectionPtr和_RecordsetPtr,后续换底层驱动、加连接池、做日志拦截,都只改某一层,不用满项目找 COM 调用。这里我按代码执行顺序,从入口往下拆。
2.1 站在入口看全局:AdoDB.cpp 里容易被忽略的三件事
很多 C++ 项目把AdoDB.cpp当成「随便放一个 main 函数的地方」,但在这个包里,它兼任了三件事。第一,它是整个 DLL/静态库的模块入口,负责设定模块句柄;第二,它定义了导出接口的形态,比如AdoDB.def里列出的那些符号,决定了外部程序能链接到哪些函数;第三,它把dllmain.cpp、stdafx.cpp和所有数据库类的编译单元串在一起,确保预编译头顺序正确。
这里必须提醒一个常见误区:不要在DllMain里调用CoInitialize或CoInitializeEx。系统加载 DLL 时持有 loader lock,在这个锁里做 COM 初始化轻则死锁,重则直接启动崩溃。我看到过好几个版本的 AdoDB 封装,dllmain.cpp里只保存模块句柄,这是对的;真正的 COM 初始化放到每个使用ADODatabase的线程去做,才符合 COM 的线程模型要求。如果你打算把它改成 DLL,入口里优先做的只有两件事:DisableThreadLibraryCalls避免线程附加/分离通知,以及把HINSTANCE存下来给资源加载用。
2.2 ADODatabase:连接串、事务与重连的落点
ADODatabase.h对应的是 ADO 的 Connection 对象。它暴露出来的接口一般不会太多,核心就是 Open、Close、IsOpen、ExecuteSQL、ExecuteQuery 这几个,外加一个拿原生_ConnectionPtr的访问器。
// ADODatabase.h(核心骨架,按常见封装习惯) class ADODatabase { public: ADODatabase(); virtual ~ADODatabase(); bool Open(const wchar_t* connStr); // 建立连接 void Close(); // 主动关闭连接 bool IsOpen() const; bool ExecuteSQL(const wchar_t* sql); // 执行 INSERT/UPDATE/DELETE bool ExecuteQuery(const wchar_t* sql, ADORecordset& rs); _ConnectionPtr GetConnection() const { return m_pConnection; } protected: _ConnectionPtr m_pConnection; unsigned long m_timeout; // 连接超时秒数,默认 15 };构造函数里通常会把m_pConnection = NULL,并把m_timeout设成 15。Open是真正产生 COM 接口的地方,也是最容易因为初始化顺序出问题的函数。
bool ADODatabase::Open(const wchar_t* connStr) { Close(); // 如果已有连接,先清理,避免连接串被忽略 HRESULT hr = m_pConnection.CreateInstance(__uuidof(Connection)); if (FAILED(hr)) { return false; } // 4 个参数:连接串、用户 ID、密码、打开选项 // adConnectUnspecified = -1,让 OLE DB 驱动自己决定行为 hr = m_pConnection->Open(connStr, L"", L"", adConnectUnspecified); if (FAILED(hr)) { m_pConnection = NULL; // 失败后立刻释放,防止悬空 return false; } return true; }这串代码里有三个参数细节值得注意。第一,CreateInstance(__uuidof(Connection))用的是 VC++ 的__uuidof运算符,前提是你在stdafx.h里用#import导入了msado15.dll的类型库,否则Connection这个类型根本不存在。第二,Open 的第二个和第三个参数是用户 ID 和密码,如果连接串里已经写了User ID=...;Password=...,这里传空字符串即可,传了反而可能覆盖连接串里的值。第三,adConnectUnspecified是 -1,代表不指定连接打开方式,让 OLE DB 提供者按默认行为处理;如果你明确知道要用adAsyncConnect异步连接,才需要改这里。
连接串本身是另一个黑匣子。常见写法有两种,一种是纯 OLE DB 提供者,一种是转 ODBC 驱动:
// SQL Server 走 SQLOLEDB const wchar_t* kMssqlConn = L"Provider=SQLOLEDB;Data Source=127.0.0.1;Initial Catalog=testdb;" L"User ID=sa;Password=yourpass;"; // MySQL 走 ODBC const wchar_t* kMysqlConn = L"Provider=MSDASQL;Driver={MySQL ODBC 8.0 Unicode Driver};" L"Server=127.0.0.1;Database=testdb;User=root;Password=yourpass;";如果你的目标数据库是 Oracle,连接串开头通常会换成Provider=OraOLEDB.Oracle。封装类的好处就在这儿:上层只传一个字符串,底层到底用哪个提供者、要不要重试,都由Open内部处理。事务相关的BeginTrans、CommitTrans、RollbackTrans也是这个类该管的事,因为事务边界必须绑定到 Connection 对象上,拿到业务层去写很容易出现「连接没关但事务悬空」的隐性问题。
2.3 ADORecordset:游标、锁策略与字段取值的权衡
ADORecordset是查询结果集的核心封装。打开一个 Recordset 之前,必须先确定游标类型和锁策略,这两个参数会直接影响你能否滚动、能否原地修改数据,以及并发写时的行为。
| 游标类型 | 常量值 | 典型用途 |
|---|---|---|
| adOpenForwardOnly | 0 | 默认,只能向前,内存占用最低 |
| adOpenKeyset | 1 | 可前后滚动,看到别人对已有记录的修改 |
| adOpenDynamic | 2 | 动态游标,看到所有增删改 |
| adOpenStatic | 3 | 静态快照,常用于分页显示 |
锁策略通常配合游标一起决定。adLockReadOnly适合纯查询,adLockPessimistic适合编辑,adLockOptimistic在 Update 时才锁。封装类通常会把这两个参数暴露出来,但默认给adOpenForwardOnly + adLockReadOnly,因为这是性能最好的组合;需要滚动或编辑时再显式指定,否则你会为没用到的能力多付内存和数据库端开销。
字段读取是另一个看点。直接操作_RecordsetPtr时,拿到的是一个VARIANT,你得自己判断vt类型再取值。封装类的意义在于把这层判断收敛到一个函数里:
// ADORecordset.cpp 内部核心读取逻辑 VARIANT var = m_pRecordset->Fields->GetItem((long)index)->GetValue(); if (var.vt == VT_I4) { return var.lVal; // 32 位整型字段 } else if (var.vt == VT_BSTR) { const wchar_t* sz = var.bstrVal; // 转成 std::wstring 再交给上层 }这里有个工程经验:按字段名取值和按下标取值,性能差距在宽表场景下非常明显。Fields->GetItem(L"user_name")每次都要做一次名字到序号的查找,如果你在 10 万行结果集的循环里反复调它,时间会翻好几倍。所以我一般会在进入循环前先把字段序号缓存下来,循环体内只按 index 取。封装类里如果同时提供GetFieldValueByName和GetFieldValueByIndex,优先用后者,并配合一个字段映射结构。
2.4 ADOCommand 与 DBError:命令执行和错误兜底的最后一块拼图
ADOCommand负责执行带参数的 SQL 或存储过程。为什么需要单独一个类?因为ADODatabase::ExecuteSQL只适合处理没有返回值的简单语句,一旦涉及参数化查询,就必须用 Command 的 Parameters 集合。参数化不仅能防 SQL 注入,还能让数据库端复用执行计划,性能比拼字符串高一个量级。
// 调用存储过程,参数化传值 ADOCommand cmd(db.GetConnection()); cmd.SetCommandText(L"{CALL sp_get_user(?)}"); cmd.AddParameter(L"@id", adInteger, 1); cmd.Execute();AddParameter的三个参数分别对应参数名、数据类型和方向(输入/输出)。注意 ADO 里?是位置占位符,@id是给参数集合命名的,两者通过顺序对应,不是通过名字匹配。存储过程有输出参数时,要显式设adParamOutput,然后从 Parameters 集合里按名字取回。
DBError则是把 COM 异常翻译成人话的东西。ADO 抛出的_com_error里只有 HRESULT,真正有用的错误描述藏在IErrorInfo里。常见封装是包一层DBError,把 HRESULT、来源、描述和出错步骤一起存下来。
// DBError.h class DBError : public std::exception { public: DBError(HRESULT hr, const wchar_t* action); virtual const char* what() const; long ErrorCode() const; };实现在绝大多数场景下就是调_com_error::ErrorMessage()或IErrorInfo::GetDescription(),然后把action字符串拼进消息里。别小看这个类,没有它,你面对的是「0x80040E14」这种数字,有它之后你至少能看到「INSERT 语句失败,字段 user_name 不能为空」这种可定位的信息。项目里所有数据库操作都该包一层这个错误处理,而不是让 HRESULT 裸奔到业务层。
3. 把 AdoDB 接进自己的 VS2013 工程:sln 编译、类型库注册与双平台配置
从压缩包里拿到的是一整套 VS2013 解决方案,但很多人直接打开AdoDB_vs2013.sln编译就报错。问题通常不在源码,而在编译前少做了两步:注册 ADO 类型库、调整#import路径。这章把整个接入流程按顺序捋一遍。
3.1 先在 stdafx.h 里把类型库注册和导入做对
ADO 是 COM 接口,它对外提供的是类型库而不是 C++ 头文件。所以在任何文件里使用_ConnectionPtr之前,必须先让编译器看到msado15.dll的类型定义,标准做法是用#import指令把它导入。但#import有几个默认行为需要手动改掉,否则编译期就会踩坑。
regtlibv12 "C:\Program Files\Common Files\System\ado\msado15.dll"这条命令的作用是把 ADO 类型库注册到系统注册表。很多精简版系统或重装过 Office 的机器上,类型库信息是缺失的,不执行这一步,#import会直接报「未找到类型库」或「无法打开文件 msado15.dll」。注意这行命令只注册类型库,不注册 COM 组件,所以它和regsvr32是两码事。如果终端提示找不到regtlibv12,去C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin找一下,或者从更高版本的 Windows SDK 里拿同名工具。
类型库注册完,接下来在stdafx.h里导入定义:
// stdafx.h #import "C:\\Program Files\\Common Files\\System\\ado\\msado15.dll" \ no_namespace rename("EOF", "adoEOF")no_namespace是让所有 ADO 类型直接暴露在全局命名空间,省去写ADODB::ConnectionPtr这类前缀。rename("EOF", "adoEOF")是必须写的,因为 C 运行时头文件里已经有一个EOF宏,和 Recordset 的EOF属性撞名,不重命名的话编译时到处都是莫名其妙的宏替换错误。如果你拿到的是别人的源码,先看一眼他的stdafx.h里有没有这两行,没有就补上,绝大多数编译失败都出在这一步。
3.2 自己工程里引入源码模块的方式
把 AdoDB 接进你现有工程有两种方式,一种是直接把 cpp 文件加进你的项目,一种是先编译成 lib 再链接。前者适合调试,后者适合复用。
先看压缩包里的解决方案能不能直接编译:
devenv AdoDB_vs2013.sln /Rebuild "Release|Win32"devenv是 Visual Studio 自带的命令行编译入口,/Rebuild表示先清理再编译。编译产物通常是Release\AdoDB.lib和Release\AdoDB.dll。如果你的环境里没有 VS2013 而是更高版本,打开 sln 时 Visual Studio 会提示做一次单向升级,一般能顺利转换;但要注意升级后 C++ 标准库的默认行为有变化,比如迭代器调试和异常掩码,最好先编译一次看有没有兼容性报错。
如果只想把自己业务 cpp 和 AdoDB 源码放在同一个工程里,需要手动补充几个依赖项。ADO 依赖的 COM 库不在默认链接列表里,在stdafx.h或AdoDB.h里补上:
#pragma comment(lib, "ole32.lib") #pragma comment(lib, "oleaut32.lib") #pragma comment(lib, "AdoDB.lib")ole32.lib提供CoInitialize、CoCreateInstance这些 COM 基础函数,oleaut32.lib提供VariantClear、SysAllocString等自动化函数,缺一个都会在链接阶段报一堆unresolved external symbol。AdoDB.lib则在你选择链接静态库方式时使用,如果直接把 cpp 加入工程,这一行去掉。
3.3 Debug 与 Release、Win32 与 x64 的典型差异
很多人在 Debug 下跑得好好的代码,切到 Release 就崩溃,第一反应是「编译器优化出了问题」,其实大半是下面这几个配置差异导致的。
| 配置项 | Win32 Debug | Win32 Release | x64 Release |
|---|---|---|---|
| 运行库 | /MDd | /MD | /MD |
| 迭代器调试 | 默认开启 | 关闭 | 关闭 |
_CRT_SECURE_NO_WARNINGS | 常需要手动加 | 常需要手动加 | 常需要手动加 |
| msado15.dll 路径 | 系统 System32 | 系统 System32 | 注意 32/64 位重定向 |
x64 下最隐蔽的问题是注册表重定向。64 位系统上,32 位进程访问HKLM\SOFTWARE\Classes\TypeLib时会被重定向到WOW6432Node,所以你用 64 位的 regtlibv12 注册的类型库,32 位进程可能看不到,反过来也一样。遇到「类型库已注册但程序找不到」的诡异现象,先确认你的可执行文件目标平台和注册工具位数是否一致。
Release 下迭代器调试默认关闭,这对 ADORecordset 封装类的直接影响是:你如果依赖at()的越界检查来暴露 bug,Release 下会直接表现为内存访问违例而不是抛异常。所以调试数据库模块时,我习惯先跑一轮 Debug,再跑一轮 Release,两轮都过了才敢说这段代码稳定。
4. 常见问题排查:从编译报错到运行时崩溃的 5 个真实坑
这一章写的是我在实际项目里踩过的坑,每一条都对应压缩包源码里某个具体配置点。现象、原因、解决办法按顺序摆出来,遇到同类问题直接对照着看。
4.1 EOF 宏冲突:no_namespace 引入的副作用
现象:编译ADORecordset.cpp时报错,错误信息指向while (!rs->EOF),说EOF不是类成员,或者干脆被替换成了某个负数常量。更隐蔽的是编译能过,但循环条件永远不成立,结果集遍历直接跳过。
原因:C 标准库的<stdio.h>里把EOF定义成了 -1 的宏。#import时如果不重命名,ADO 的EOF属性就会被宏替换成 -1,你写的rs->EOF实际变成rs->-1,编译器当然报错。如果某个头文件包含顺序不同,宏替换发生在 ADO 类型定义之前,还可能产生更隐蔽的类型污染。
解决:#import指令里加rename("EOF", "adoEOF")。注意只能改导入语句,不能改业务代码里已有的IsEOF()调用。如果你在封装类里已经写了bool IsEOF() const { return m_pRecordset->adoEOF == VARIANT_TRUE; },那使用方完全感知不到这个改名。
4.2 0x800401F0:尚未调用 CoInitialize 的线程模型问题
现象:程序在某个工作线程里创建ADODatabase,调用 Open 时抛出异常,HRESULT 是0x800401F0,英文描述是CoInitialize has not been called。主线程用同一个类却完全正常。
原因:COM 的初始化是线程级的,不是进程级的。DllMain即使做了CoInitialize,也只能覆盖模块加载的那个线程。你把ADODatabase对象丢到线程池或者 std::thread 里时,新线程根本不在 COM 初始化范围内,任何CreateInstance都会失败。这个坑在测试环境不容易暴露,因为单线程程序永远碰不到。
解决:每个使用 ADO 的工作线程入口处显式初始化,并在线程退出前释放。最稳妥的做法是写一个 RAII 包装类:
// ComInitializer.h class ComInitializer { public: ComInitializer() { HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); m_ok = SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE; } ~ComInitializer() { if (m_ok) CoUninitialize(); } private: bool m_ok; };COINIT_MULTITHREADED表示多线程套间,ADO 通常用这个模式更安全。如果你的业务里有多个线程各自建连接,每个线程入口都放一个ComInitializer localInit;即可,不需要共享,也不需要全局。还有一个细节:CoInitializeEx返回RPC_E_CHANGED_MODE时不算失败,说明该线程已经初始化过但套间模式不同,此时不能重复调用CoUninitialize,所以上面代码里用m_ok单独记录。
4.3 Release 翻车:智能指针析构顺序与悬空引用
现象:Debug 编译运行都正常,切到 Release 后程序退出时崩溃,崩溃点经常在ADODatabase或ADORecordset的析构函数里。有时候不崩溃,但关闭连接后再次 Open,报错提示连接已被释放。
原因:_ConnectionPtr和_RecordsetPtr是引用计数的 COM 智能指针,它们的成员析构顺序和声明顺序正好相反。如果类里先声明_ConnectionPtr,后声明_RecordsetPtr,析构时会先释放 Recordset 再释放 Connection,这看起来没问题。问题出在一些封装类里,析构函数自己写了Close(),但Close()只关了连接,没有把 Recordset 里缓存的字段指针清空,导致 Recordset 还持有指向已释放接口的指针。
解决:析构顺序必须遵从「先关记录集,再关连接」的纪律,而且关闭后要把指针置 NULL。我一般会在ADODatabase::Close()里按固定顺序操作:
void ADODatabase::Close() { // 先让所有 Recordset 走完析构,再关连接 if (m_pConnection != NULL) { try { m_pConnection->Close(); } catch (_com_error&) { // 连接可能已经断开,吞掉二次异常 } m_pConnection = NULL; } }这里m_pConnection = NULL不是多余的。COM 智能指针赋值 NULL 会触发Release(),把引用计数减到 0,彻底断开和底层对象的联系。不这么写的话,Close()只是断开了网络连接,但 COM 对象还活着,下次Open()时CreateInstance拿到的是同一个未释放对象,状态已经错乱。这条在 Release 下最容易暴露,因为 Debug 的智能指针有额外的调试断言,能帮你挡掉一部分悬空访问。
4.4 中文乱码与特殊字符:BSTR 转换的隐性陷阱
现象:连接串里数据库名是中文,或者查询条件里带中文参数,结果要么连不上,要么查出来的数据在界面上显示成乱码。英文和数字一切正常。
原因:ADO 的字符串参数本质是 BSTR,也就是 Unicode 字符串。你的 C++ 代码如果用char*传中文,中间要经过一次多字节到宽字符的转换,而这次转换用的代码页取决于系统区域设置,不一定是你源码文件保存的 UTF-8。常见情况是源码是 UTF-8、系统代码页是 GBK,两边一错位,进到 ADO 里的就是乱码。#import生成智能指针时虽然提供了隐式转换,但转换规则对中文并不友好。
解决:数据库模块内部统一使用宽字符接口,只在最外层做编码转换。如果你保留char*的对外接口,转换处要显式指定代码页:
_bstr_t ConvertUtf8ToBstr(const char* utf8) { // 先转成宽字符,再构造 BSTR int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, NULL, 0); std::wstring wstr(len - 1, L'\0'); MultiByteToWideChar(CP_UTF8, 0, utf8, -1, &wstr[0], len); return _bstr_t(wstr.c_str()); }MultiByteToWideChar的第一个参数是源字符串编码,这里写CP_UTF8表示把 UTF-8 转成 UTF-16。如果你的工程里统一用 ANSI,那边改成CP_ACP也可以,但最好先把所有源文件设成带签名的 UTF-8,这样不管换到哪台机器编译,行为都一致。排查这类问题时,不要盯着 SQL 语句本身,先确认_bstr_t里的字节是什么,再往上追转换点。
4.5 regtlibv12 注册失败:文件路径与位数不匹配
现象:命令行执行regtlibv12 "C:\Program Files\Common Files\System\ado\msado15.dll"后没有任何反应,或者提示LoadLibrary failed,但在资源管理器里明明能看到这个文件存在。
原因:regtlibv12是个 32 位工具。在 64 位系统的默认命令行里执行时,文件系统重定向会把C:\Program Files\Common Files\System\ado下的 64 位msado15.dll指向一个它加载不了的路径。另外,如果你的终端不是管理员权限,写入HKLM\SOFTWARE\Classes\TypeLib时会被 UAC 拦截,命令可能静默失败。
解决:用 32 位命令提示符执行,路径认准 SysWOW64 版本。具体操作是:打开C:\Windows\SysWOW64\cmd.exe,以管理员身份运行,然后执行同样的命令。执行完后用以下方式验证类型库是否真的注册成功:
reg query HKLM\SOFTWARE\WOW6432Node\Classes\TypeLib /s | findstr /i "ADODB"输出里能看到 ADODB 相关的类型库 GUID 说明注册成功。如果你的系统是老式 32 位 Windows,直接查HKLM\SOFTWARE\Classes\TypeLib即可。还有一种更省事的方法:在stdafx.h里直接#import类型库并且让编译器生成包装类,这时即使类型库没有注册,只要msado15.dll存在,#import也能解析成功,只是生成的智能指针类名前面会多出命名空间。两种方式选一种,别叠加使用,否则可能出现两套类型定义冲突。
5. 进阶玩法:Recordset 流式分页与连接复用,让 AdoDB 用得更顺手
基础链路跑通之后,再往上走就是性能和并发问题。这里给两个可以直接抄走的做法,一个是分页读取,一个是简单的连接复用。
分页读取的核心不是LIMIT语法,而是 Recordset 自带的PageSize、AbsolutePage属性和MoveNext的组合。PageSize决定每页多少条,AbsolutePage决定跳到第几页,整个过程由 ADO 在服务端完成数据定位:
// 分页读取核心逻辑 rs.SetPageSize(1000); // 每页 1000 条 long pageCount = rs.GetPageCount(); // 总页数,-1 表示无法确定 rs.SetAbsolutePage(1); // 跳到第 1 页,从 1 开始计数 for (int i = 0; i < 1000 && !rs.IsEOF(); ++i) { // 读取当前页字段 long id = rs.GetFieldValueLong(L"id"); // 业务处理... rs.MoveNext(); }SetAbsolutePage跳页后游标定位到该页第一条记录,循环里用MoveNext逐步走完当前页。注意pageCount只有在支持静态游标时才能算出来,adOpenForwardOnly游标调用GetPageCount会返回 -1,所以分页场景必须用adOpenStatic或adOpenKeyset。
连接复用我一般做成一个极简的连接池。ADO 的_ConnectionPtr本身有引用计数,但频繁Open/Close的开销依然很大,尤其是走 ODBC 驱动时每次都要重新认证和分配句柄。简单的池化思路是:把用完的连接挂回空闲列表,下次取出时先验证还能用,不能用再重建。
class SimpleConnPool { public: ADODatabase* Acquire() { if (!m_free.empty()) { ADODatabase* db = m_free.back(); m_free.pop_back(); if (Ping(*db)) { return db; } delete db; // 连接已失效,丢掉重建 } ADODatabase* db = new ADODatabase(); db->Open(m_connStr.c_str()); return db; } void Release(ADODatabase* db) { m_free.push_back(db); // 不 Close,留给下一次 Acquire } private: bool Ping(ADODatabase& db) { ADORecordset rs; return db.ExecuteQuery(L"SELECT 1", rs); } std::string m_connStr; std::vector<ADODatabase*> m_free; };Ping是我习惯性加的验证手段,SELECT 1是最轻量的连通性测试。这个池子没有加锁,适合单线程或者外面已经串行化的场景;多线程并发时要给Acquire/Release加锁,或者改用std::mutex保护m_free,否则会出现两个线程拿到同一个连接的问题。连接池的上限也必须控制,否则数据库端连接数会被打满。
关于验证,再补一条建议:任何一次对数据库模块的改动,我都会先在入口处跑一段最小链路——连接、执行、读一个字段、主动释放。这段链路能通过,才说明 ADODatabase 和 ADORecordset 的生命周期管理没被改坏。我从那次 Release 崩溃之后,每次拿到封装好的库都强制走一遍这个最小测试,包括换编译器版本、换平台架构这类看似无关的变动也不能跳过。这个习惯帮我挡掉了不少看似玄学、其实是生命周期问题的崩溃。希望帮到你。
本文还有配套的精品资源,点击获取