尧图精选

MFC工程中ADO封装实战:从AdoWrapper到参数化查询与事务

🕒 发布时间:2026/9/26 21:26:19 📁 来源:尧图网络
简介这份资源是面向商业软件开发者的 ADO 数据库编程实战源码包以 Ado_Aok_demo 示例项目为核心帮助开发者理解 ActiveX Data Objects 在真实业务系统中的落地方式。压缩包共 26 个文件约 41KB以 8 个 .h 头文件与 6 个 .cpp 源文件为主体配合 .dsw、.dsp 工程文件、.rc 资源脚本、.ico 与 .bmp 图标位图以及 msado15.tlh、msado15.tli 等 ADO 类型库导入文件构成一套可直接编译研究的 MFC 工程骨架。源码覆盖连接管理、命令执行、Recordset 数据读取、参数化查询、错误处理、事务控制与数据增删改等关键环节并涉及批处理、Stream 对象等进阶用法。已有 125 人学习适合希望提升数据库编程能力、为财务、库存或客户关系管理等商业应用打基础的开发者参考借鉴。1. 从一份 MFC 工程拆开 ADO 的真实用法接手一套跑了七八年的老库存系统时最头疼的不是业务逻辑而是数据访问层里散落各处的_ConnectionPtr、_RecordsetPtr和一堆没释放的 COM 指针。翻遍代码想找个能跑通的 ADO 参考最后在Ado_Aok_demo.zip里找到了答案。这是一份基于 MFC 的 ADO 源代码示例工程名 AdoTest核心封装在AdoWrapper.h里配合msado15.tlh和msado15.tli两个由#import自动生成的智能指针声明文件把连接、命令、结果集、事务这几件事串成了一条完整链路。它解决的不是ADO 是什么这种概念问题而是在商业项目里 ADO 到底该怎么组织、怎么封装、怎么不出内存泄漏这种落地问题。适合正在维护 VC6 或 VS 老工程、需要把 ADO 用规范的人也适合想通过一份可编译源码理解 COM 智能指针生命周期的开发者。2. 工程结构与 ADO 封装层拆解从 dsp 到 AdoWrapper.h2.1 先认清这套 MFC 工程的骨架拿到压缩包解压后第一眼看到的是一堆 VC6 时代的工程文件。AdoTest.dsp和AdoTest.dsw是项目文件和工作区文件用 VC6 或 VS 2008 以上版本打开时VS 会提示升级工程格式这一步正常走就行。AdoTest.clw是 ClassWizard 的类信息文件AdoTest.plg是编译日志这两个文件不影响编译但保留着能看出原始开发环境的痕迹。真正要关注的是源码文件的分层。AdoTest.cpp/AdoTest.h是应用入口MainFrm.cpp/MainFrm.h管主框架窗口AdoTestDoc.cpp/AdoTestDoc.h是文档类AdoTestView.cpp/AdoTestView.h是视图类CntrItem.cpp/CntrItem.h处理 OLE 容器项。这是标准 MFC 文档/视图架构数据访问逻辑不会散在这些文件里而是集中在AdoWrapper.h。StdAfx.cpp/StdAfx.h是预编译头resource.h和AdoTest.rc管资源res目录下放着AdoTest.ico、AdoTestDoc.ico、Toolbar.bmp、AdoTest.rc2这些图标和工具栏位图。ReadMe.txt通常写着工程说明和编译注意事项动手前先扫一眼。关键点在于msado15.tlh和msado15.tli。这两个文件不是手写的是编译器处理#import C:\Program Files\Common Files\System\ado\msado15.dll no_namespace rename(EOF, EndOfFile)这类指令时自动生成的。.tlh里是 ADO 类型库的头文件声明.tli是内联实现。很多人第一次看到工程里有这两个文件会懵以为是第三方库其实它们是构建产物但保留在工程里能让没装对应 SDK 的机器也能编译通过。2.2 AdoWrapper.h 里的连接与命令封装AdoWrapper.h是这份源码最值得逐行读的部分。它把 ADO 的三个核心智能指针——_ConnectionPtr、_CommandPtr、_RecordsetPtr——包在一个类里对外暴露简洁的接口。下面这段是典型的连接管理写法我按原工程的思路还原出来// AdoWrapper.h 核心片段连接管理与命令执行 #import C:\Program Files\Common Files\System\ado\msado15.dll \ no_namespace rename(EOF, EndOfFile) class CAdoWrapper { public: CAdoWrapper() : m_bConnected(FALSE) {} ~CAdoWrapper() { Close(); } // 打开连接connectionString 形如 // ProviderSQLOLEDB;Data Source127.0.0.1;Initial CatalogTestDB;User IDsa;Passwordxxx; BOOL Open(LPCTSTR connectionString) { if (m_bConnected) return TRUE; try { HRESULT hr m_pConnection.CreateInstance(__uuidof(Connection)); if (FAILED(hr)) return FALSE; m_pConnection-ConnectionTimeout 10; // 连接超时单位秒 m_pConnection-CommandTimeout 30; // 命令超时单位秒 m_pConnection-Open(_bstr_t(connectionString), , , adConnectUnspecified); m_bConnected TRUE; return TRUE; } catch (_com_error e) { // e.ErrorMessage() 能拿到 ADO 返回的中文错误描述 TRACE(_T(ADO Open failed: %s\n), (LPCTSTR)e.Description()); return FALSE; } } // 执行不返回结果集的 SQL如 INSERT/UPDATE/DELETE BOOL ExecuteNonQuery(LPCTSTR sql, long* pAffected) { if (!m_bConnected) return FALSE; try { _variant_t affected; m_pConnection-Execute(_bstr_t(sql), affected, adExecuteNoRecords); if (pAffected) *pAffected affected.lVal; return TRUE; } catch (_com_error e) { TRACE(_T(ExecuteNonQuery failed: %s\n), (LPCTSTR)e.Description()); return FALSE; } } // 执行查询返回 Recordset 智能指针 _RecordsetPtr ExecuteQuery(LPCTSTR sql) { if (!m_bConnected) return NULL; try { return m_pConnection-Execute(_bstr_t(sql), NULL, adCmdText); } catch (_com_error e) { TRACE(_T(ExecuteQuery failed: %s\n), (LPCTSTR)e.Description()); return NULL; } } void Close() { if (m_pConnection m_bConnected) { m_pConnection-Close(); m_bConnected FALSE; } } private: _ConnectionPtr m_pConnection; BOOL m_bConnected; };这段代码的逻辑说明CreateInstance通过__uuidof(Connection)拿到 ADO 连接对象的 CLSID 并创建实例这是 COM 智能指针的标准创建方式。ConnectionTimeout和CommandTimeout两个参数在商业项目里必须设默认值在某些网络环境下会让界面卡死几十秒。Open的第三个和第四个参数传空字符串表示使用连接字符串里已有的认证信息adConnectUnspecified表示同步打开。Execute的第二个参数接收受影响行数第三个参数adExecuteNoRecords告诉 ADO 这条语句不返回结果集能省掉一次不必要的 Recordset 创建。参数说明连接字符串里的ProviderSQLOLEDB是 SQL Server 的 OLE DB 提供程序如果连的是 Oracle 要换成MSDAORA连 Access 用Microsoft.Jet.OLEDB.4.0。Data Source可以写 IP 也可以写实例名Initial Catalog是默认数据库。User ID和Password如果走 Windows 集成认证可以换成Integrated SecuritySSPI并去掉账号密码。2.3 结果集遍历与字段读取的规范写法拿到_RecordsetPtr之后怎么读数据是新手最容易翻车的地方。EOF被#import重命名成了EndOfFile因为 MFC 里已经有EOF宏不重命名会冲突。遍历时用while (!rs-EndOfFile)移动用rs-MoveNext()。字段读取有三种方式rs-Fields-GetItem(列名)-Value、rs-GetCollect(列名)、rs-Fields-Item[0]-Value。第一种最直观但每次都要查字段名循环里性能差第二种是GetCollect封装稍快第三种按下标取最快但可读性差。// 遍历结果集并读取字段 _RecordsetPtr rs m_ado.ExecuteQuery(_T(SELECT id, name, amount FROM orders)); if (rs ! NULL) { while (!rs-EndOfFile) { long id rs-Fields-GetItem(_T(id))-Value; _bstr_t name rs-Fields-GetItem(_T(name))-Value; double amount rs-Fields-GetItem(_T(amount))-Value; // 处理一行数据 rs-MoveNext(); } rs-Close(); // 用完必须关闭 }这里有个血泪经验_variant_t到具体类型的转换在字段为 NULL 时会抛异常。商业库里字段允许 NULL 是常态所以读之前要先判断rs-Fields-GetItem(列名)-Value.vt VT_NULL或者用_variant_t的ChangeType配合异常捕获。原工程在AdoWrapper.h里对这点做了封装读字段时统一走一个模板函数NULL 时返回默认值。3. 编译运行与数据库联调从 dsp 升级到第一个查询跑通3.1 工程升级与依赖配置用 VS2010 及以上版本打开AdoTest.dsw会弹出升级 VC 项目向导确认即可。升级后.dsp变成.vcxproj但源码文件不变。接下来要确认msado15.dll的路径。32 位系统在C:\Program Files\Common Files\System\ado\64 位系统在C:\Program Files (x86)\Common Files\System\ado\。如果#import指令里的路径写死了 32 位路径在 64 位机器上编译会报无法打开类型库文件。常见做法是把#import路径改成相对路径或者用环境变量但更省事的办法是直接在工程属性里配置。右键工程 → 属性 → C/C → 常规 → 附加包含目录把 ADO 目录加进去然后把#import改成#import msado15.dll no_namespace rename(EOF, EndOfFile)让编译器自己去系统目录找。编译时如果报msado15.tlh相关的重复定义错误检查是不是同时存在手动保留的.tlh和自动生成的.tlh。VS 升级工程后旧的.tlh/.tli可能和新生成的冲突删掉工程目录下的这两个文件重新编译即可。3.2 连接字符串的三种写法与测试连接字符串是 ADO 联调的第一道坎。下面三种写法覆盖了最常见的场景// 写法一SQL Server 账号密码认证 LPCTSTR conn1 _T(ProviderSQLOLEDB;Data Source192.168.1.100,1433;) _T(Initial CatalogAdoTestDB;User IDsa;PasswordYourPwd;); // 写法二SQL Server Windows 集成认证 LPCTSTR conn2 _T(ProviderSQLOLEDB;Data Source192.168.1.100;) _T(Initial CatalogAdoTestDB;Integrated SecuritySSPI;); // 写法三Access 本地文件 LPCTSTR conn3 _T(ProviderMicrosoft.Jet.OLEDB.4.0;) _T(Data SourceC:\\data\\test.mdb;);参数说明Data Source后面跟,1433是显式指定端口SQL Server 默认端口就是 1433不写也行。Integrated SecuritySSPI表示用当前 Windows 登录账号去连数据库不需要在字符串里写账号密码但要求数据库服务器和客户端在同一个域或者有信任关系。Access 的Microsoft.Jet.OLEDB.4.0只支持 32 位程序64 位程序要换成Microsoft.ACE.OLEDB.12.0并且要装对应的 Access Database Engine。测试连接是否成功最直接的办法是在AdoTestView.cpp的某个按钮响应里调用m_ado.Open(conn1)返回 TRUE 就说明通了。如果返回 FALSE看TRACE输出的错误描述。常见的错误码0x80004005一般是连接字符串写错或者数据库服务没启动0x80040E4D是登录失败0x80040E14是 SQL 语法错误。3.3 把查询结果绑到 MFC 列表控件跑通连接之后下一步是把_RecordsetPtr里的数据填到CListCtrl里。这是商业项目里最常见的展示需求。思路是先在视图类里加一个CListCtrl成员初始化时插入列头查询返回后清空列表再逐行插入。// 在 AdoTestView.cpp 中填充列表控件 void CAdoTestView::RefreshOrderList() { m_list.DeleteAllItems(); // 清空旧数据 _RecordsetPtr rs m_ado.ExecuteQuery( _T(SELECT id, name, amount FROM orders ORDER BY id)); if (rs NULL) return; int nItem 0; while (!rs-EndOfFile) { long id rs-Fields-GetItem(_T(id))-Value; _bstr_t name rs-Fields-GetItem(_T(name))-Value; double amount rs-Fields-GetItem(_T(amount))-Value; CString strId, strAmount; strId.Format(_T(%ld), id); strAmount.Format(_T(%.2f), amount); m_list.InsertItem(nItem, strId); m_list.SetItemText(nItem, 1, (LPCTSTR)name); m_list.SetItemText(nItem, 2, strAmount); nItem; rs-MoveNext(); } rs-Close(); }逻辑说明DeleteAllItems清空列表InsertItem插入第一列并返回行号SetItemText设置后续列。_bstr_t到LPCTSTR的转换在 Unicode 工程下要注意_bstr_t内部是宽字符直接强转在 Unicode 配置下没问题但在多字节配置下会乱码。稳妥做法是用(LPCTSTR)_bstr_t或者_bstr_t的operator LPCTSTR。参数说明ORDER BY id保证每次查询顺序一致避免列表刷新时行序跳动。如果数据量大不要一次性全查出来塞列表用分页查询每页 100 到 500 条配合CListCtrl的虚拟列表模式。4. 避坑与排查ADO 在 MFC 工程里的五个真实翻车点4.1 现象程序退出时崩溃报 access violation原因_ConnectionPtr和_RecordsetPtr是 COM 智能指针在 MFC 的ExitInstance或析构函数里如果 COM 库已经CoUninitialize了智能指针再释放就会访问已卸载的 COM 组件。常见于在App类的ExitInstance里关闭连接但CoUninitialize在更早的地方被调用了。解决把 ADO 连接的关闭放在CoUninitialize之前或者干脆在CAdoWrapper的析构里只做Close不依赖 COM 库的卸载顺序。更稳妥的做法是在InitInstance里CoInitialize(NULL)在ExitInstance里先关闭所有 ADO 对象再CoUninitialize。4.2 现象查询返回的 Recordset 是 NULL但 SQL 在数据库客户端能跑通原因Execute的第三个参数用了adExecuteNoRecords这个参数告诉 ADO 不返回结果集只用于 INSERT/UPDATE/DELETE。如果 SELECT 语句也带这个参数返回的 Recordset 就是空的。解决查询用adCmdText非查询用adExecuteNoRecords。在AdoWrapper.h里把ExecuteQuery和ExecuteNonQuery分开各自用正确的参数。4.3 现象中文数据读出来是乱码原因数据库字段是nvarchar但连接字符串里没指定字符集或者_bstr_t到CString的转换在 Unicode/多字节配置下不匹配。VC6 工程默认多字节VS 升级后默认 Unicode两边行为不一致。解决统一工程字符集。在工程属性 → 配置属性 → 常规 → 字符集中选使用 Unicode 字符集。连接字符串里加;Character SetUTF-8或;Character SetGB2312视数据库而定。读字段时用_variant_t的ChangeType(VT_BSTR)再转CString。4.4 现象长时间运行后内存持续增长原因_RecordsetPtr用完没Close或者Execute返回的 Recordset 在循环里被反复创建但没释放。COM 对象引用计数不为零内存就不会回收。解决每个ExecuteQuery返回的 Recordset 用完必须Close最好用 RAII 封装一个CRecordsetGuard构造时接收_RecordsetPtr析构时自动Close。AdoWrapper.h里如果没做这层封装自己补一个。4.5 现象多线程里调用 ADO 报未指定的错误原因ADO 的 COM 对象默认是单元线程模型STA在一个线程里创建的连接不能在另一个线程里直接用。MFC 界面线程和工作者线程之间传递_ConnectionPtr会出问题。解决每个线程用自己的CAdoWrapper实例各自CoInitialize。不要跨线程共享连接对象。如果必须共享用连接池或者把数据库操作集中到一个线程里其他线程通过消息队列请求。5. 从 AdoWrapper 到参数化查询与事务把 demo 改成能上生产的代码Ado_Aok_demo里的AdoWrapper.h已经搭好了连接和查询的架子但直接拿去接商业项目还差两步参数化查询和事务控制。这两件事不做SQL 注入和部分写入失败就是迟早的事。参数化查询的核心是_CommandPtr。不要用字符串拼接 SQL而是用Command对象的Parameters集合。下面这段是改造后的写法// 参数化查询防止 SQL 注入 _CommandPtr CAdoWrapper::CreateCommand(LPCTSTR sql) { _CommandPtr cmd; cmd.CreateInstance(__uuidof(Command)); cmd-ActiveConnection m_pConnection; cmd-CommandText _bstr_t(sql); cmd-CommandType adCmdText; return cmd; } // 调用示例按 name 查询 _CommandPtr cmd m_ado.CreateCommand( _T(SELECT id, amount FROM orders WHERE name ?)); cmd-Parameters-Append(cmd-CreateParameter( _T(name), adVarWChar, adParamInput, 50, _variant_t(_T(张三)))); _RecordsetPtr rs cmd-Execute(NULL, NULL, adCmdText);参数说明CreateParameter的五个参数分别是参数名、数据类型、参数方向、长度、值。adVarWChar对应nvarcharadParamInput表示输入参数。Append的顺序要和 SQL 里?的顺序一致。Execute的第一个参数是受影响行数第二个是参数第三个是命令类型。事务控制用_ConnectionPtr的BeginTrans、CommitTrans、RollbackTrans。商业场景里一批 INSERT/UPDATE 要么全成功要么全回滚必须包在事务里// 事务处理批量更新要么全成要么全滚 BOOL CAdoWrapper::BatchUpdate() { try { m_pConnection-BeginTrans(); ExecuteNonQuery(_T(UPDATE account SET balance balance - 100 WHERE id 1)); ExecuteNonQuery(_T(UPDATE account SET balance balance 100 WHERE id 2)); m_pConnection-CommitTrans(); return TRUE; } catch (_com_error e) { m_pConnection-RollbackTrans(); TRACE(_T(BatchUpdate rolled back: %s\n), (LPCTSTR)e.Description()); return FALSE; } }这里有个细节BeginTrans之后如果ExecuteNonQuery内部自己捕获了异常并返回 FALSE外层不会知道事务会继续往下走。所以事务里的每个操作要么直接抛异常要么把返回值检查加上一旦有失败立即RollbackTrans。验证改造是否成功我一般会做三个测试第一用单引号构造的恶意字符串做参数看查询是否仍然安全第二在事务中间人为制造一个错误比如更新一个不存在的表看是否回滚第三连续跑一千次查询看内存是否稳定。从那以后我每次拿到一份 ADO 示例代码都强制走一遍参数化和事务改造再上生产环境。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →