ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Ado_Aok_demo源码解析:ADO数据库连接、查询与避坑实战

Ado_Aok_demo源码解析:ADO数据库连接、查询与避坑实战 简介这份资源是面向商业软件开发者的 ADO 数据库编程实战源码包以 Ado_Aok_demo 示例项目为核心帮助开发者理解 ActiveX Data Objects 在真实业务系统中的落地方式。内容围绕数据库连接管理、命令执行、结果集遍历、参数化查询、错误处理与事务控制等关键环节展开适合正在开发财务、库存或客户关系管理等数据驱动型应用的中级程序员参考。压缩包共 26 个文件约 41KB以 8 个 h 头文件与 6 个 cpp 源文件为主体另含 ico、bmp 等界面资源、dsw/dsp 工程文件及 msado15.tlh、msado15.tli 等 ADO 类型库导入文件完整呈现一个可编译的 MFC 工程结构。目前已有 125 人学习。通过研读源码读者可掌握连接字符串配置、Recordset 数据读取、参数传递防注入、事务提交回滚等实用技巧并借鉴其错误检查与异常处理思路为构建稳定高效的商业数据库应用积累可直接复用的代码经验。1. 从 Ado_Aok_demo.zip 说起一个 ADO 源码包到底能解决什么问题很多人第一次看到Ado_Aok_demo.zip这种命名第一反应是「这不就是个老掉牙的数据库示例吗」。但如果你手上正好有一个需要快速接数据库的 C/MFC 小工具、一个内部报表程序或者一个要交付给客户但不想引入 ORM 框架的桌面端项目这个包的价值就出来了它把 ADOActiveX Data Objects连接、查询、遍历记录集、处理字段这一整套动作用一份能直接编译的源码摊开给你看。ADO 本身是建立在 OLE DB 之上的高层接口用 COM 组件封装了数据库访问好处是 Windows 原生、依赖少、上手快坏处是坑都藏在 COM 初始化和字段类型转换里。这篇不是讲 ADO 是什么的科普而是顺着Ado_Aok_demo.zip这个源码包把「怎么把它跑起来、怎么改成自己的库、参数怎么设、哪里会翻车」讲清楚。适合有 C 基础、需要在 Windows 上快速接 SQL Server 或 Access 的开发者也适合想看懂老项目里 ADO 代码的人。2. 拆开 Ado_Aok_demo.zip源码结构、依赖与最小可运行路径2.1 先看清包里有什么再决定怎么编译拿到Ado_Aok_demo.zip之后不要急着双击 sln。先解压用文件管理器或tree看一眼目录。常见的 ADO demo 包结构大致是这几类文件工程文件.sln、.vcxproj或老的.dsp、主实现文件Ado_Aok_demo.cpp、Ado_Aok_demoDlg.cpp之类、头文件、资源文件.rc、.ico以及可能的数据库文件.mdb、.accdb或建表脚本.sql。先确认有没有数据库文件这决定了你是直接跑还是要自己建库。# 解压后先看目录结构确认工程类型和数据库文件 unzip -l Ado_Aok_demo.zip # 如果已经解压用 tree 看层级Windows 可用 dir /s /b tree -L 3 Ado_Aok_demo上面第一条命令只列压缩包内容不落盘适合先侦察第二条看已解压目录。重点看三样有没有.sln/.vcxproj决定用哪个 VS 版本、有没有.mdb/.accdb/.sql决定数据源、有没有#import指令决定 ADO 库怎么引入。如果只有.cpp没有工程文件那就要自己新建工程再把这些文件拖进去。2.2 ADO 的引入方式#import还是纯 COM选哪个ADO 在 C 里接入有两种主流做法。第一种是用#import引入msado15.dll编译器自动生成包装类和智能指针写起来最省事Ado_Aok_demo这类包大多用这种。第二种是纯 COM 手动CoCreateInstance不依赖类型库适合不想让编译器生成.tlh/.tli的场景。新手直接用第一种熟手在跨编译器或需要精细控制时才用第二种。// 方式一#import最省事demo 包基本都用这个 #import C:\Program Files\Common Files\System\ado\msado15.dll \ no_namespace rename(EOF, EndOfFile) // 注意 renameADO 的 EOF 和标准库/其他头文件的 EOF 会冲突必须改名 // 方式二纯 COM不生成包装类适合特殊环境 // CoInitialize(NULL); // CLSID clsid; // CLSIDFromProgID(LADODB.Connection, clsid); // CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IDispatch, (void**)pConn);#import那行的路径要按你机器上msado15.dll的实际位置改32 位和 64 位系统路径可能不同。rename(EOF, EndOfFile)不是可选项是必须项否则编译时EOF宏冲突会让你怀疑人生。纯 COM 方式里CoInitialize必须在任何 ADO 调用之前执行且每个线程都要初始化一次这是后面避坑章要展开的点。2.3 连接字符串Provider 选错后面全白搭ADO 连接的核心是连接字符串。SQL Server 用SQLOLEDB或MSOLEDBSQLAccess 用Microsoft.Jet.OLEDB.4.032 位或Microsoft.ACE.OLEDB.12.0需装 Access Database Engine。选错 Provider 的典型现象是报「未找到提供程序」而不是报密码错很多人会误以为是数据库问题。// SQL Server 连接示例 CString strConn _T(ProviderMSOLEDBSQL;Server127.0.0.1,1433;); strConn _T(DatabaseTestDB;UIDsa;PWDyour_password;TrustServerCertificateyes;); // Access 连接示例注意位数要和进程匹配 // CString strConn _T(ProviderMicrosoft.ACE.OLEDB.12.0;Data Source./data.mdb;); try { m_pConn.CreateInstance(__uuidof(Connection)); // 创建连接对象 m_pConn-Open(_bstr_t(strConn), , , adConnectUnspecified); } catch (_com_error e) { // e.Description() 才是真正有用的错误描述别只看 e.ErrorMessage() CString msg (LPCTSTR)e.Description(); AfxMessageBox(msg); }Server后面跟端口用逗号不是冒号这是 SQL Server 连接串的老规矩。TrustServerCertificateyes在本地自签证书环境能省掉 SSL 报错。Access 的 Provider 位数必须和你的进程位数一致64 位程序连 32 位 Jet 一定失败这是最高频的翻车点之一。_com_error里Description()比ErrorMessage()信息量大得多排错时优先看它。2.4 查询与遍历记录集把 demo 的查询改成你自己的demo 包里最值得抄的就是记录集遍历这段。_RecordsetPtr打开后用EndOfFile判断结束Fields-GetItem()取字段MoveNext()前进。改成自己的查询只需要换 SQL 和字段名。_RecordsetPtr pRs; try { pRs.CreateInstance(__uuidof(Recordset)); // adOpenStatic adLockReadOnly 是最稳的只读遍历组合 pRs-Open(_bstr_t(SELECT id, name, amount FROM orders WHERE status1), m_pConn.GetInterfacePtr(), adOpenStatic, adLockReadOnly, adCmdText); while (!pRs-EndOfFile) { // 注意这里用的是 rename 后的 EndOfFile long id pRs-Fields-GetItem(id)-Value; CString name (LPCTSTR)(_bstr_t)pRs-Fields-GetItem(name)-Value; double amount pRs-Fields-GetItem(amount)-Value; // 处理这一行…… pRs-MoveNext(); } pRs-Close(); } catch (_com_error e) { CString msg (LPCTSTR)e.Description(); AfxMessageBox(msg); }游标类型adOpenStatic适合只读遍历性能比adOpenDynamic好且不会因为别人改数据而乱跳。adLockReadOnly避免加锁。字段取值时Value是_variant_t直接赋给long/double会走隐式转换遇到 NULL 会抛异常生产代码里要先判断vt VT_NULL。GetItem用字段名比用索引可读但索引更快字段多、循环量大时用索引。3. 把 Ado_Aok_demo 改成生产可用参数、事务与错误处理3.1 三个必调参数游标、锁、命令类型很多人把 demo 跑通就直接上生产结果数据一多就卡、并发一上来就锁。问题基本出在Open的三个参数上。游标类型决定记录集怎么取数据锁类型决定并发行为命令类型决定 ADO 怎么解析你的 SQL。参数常用值适用场景代价游标类型adOpenStatic只读遍历、报表数据快照不反映他人修改游标类型adOpenForwardOnly单向快速遍历不能回退最省资源锁类型adLockReadOnly纯查询不加锁并发最好锁类型adLockOptimistic需要改数据乐观锁冲突时更新失败命令类型adCmdText普通 SQL每次解析命令类型adCmdStoredProc存储过程需正确设参数选型逻辑很简单只读就用adOpenStaticadLockReadOnlyadCmdText要改数据且并发不高用adLockOptimistic高频只读遍历用adOpenForwardOnly。别默认用adOpenDynamic它开销大且在很多 Provider 上退化成静态游标属于「看着高级实际没用」的典型。3.2 参数化查询别用字符串拼接SQL 注入和类型错误都在这demo 里为了简单常常直接拼 SQL生产环境必须改成参数化。ADO 的Command对象支持Parameters-Append既防注入又避免日期、字符串转义问题。_CommandPtr pCmd; pCmd.CreateInstance(__uuidof(Command)); pCmd-ActiveConnection m_pConn; pCmd-CommandText INSERT INTO orders (name, amount, created) VALUES (?, ?, ?); pCmd-CommandType adCmdText; // 按顺序 Append类型要和数据库列匹配 pCmd-Parameters-Append(pCmd-CreateParameter(name, adVarWChar, adParamInput, 50, _bstr_t(张三))); pCmd-Parameters-Append(pCmd-CreateParameter(amount, adDouble, adParamInput, sizeof(double), 99.5)); pCmd-Parameters-Append(pCmd-CreateParameter(created, adDBTimeStamp, adParamInput, sizeof(DATE), dt)); pCmd-Execute(NULL, NULL, adCmdText);?占位符的顺序必须和Append顺序一致这是 ADO 参数化的硬规则不像有些库支持命名参数。adVarWChar对应 nvarcharadDouble对应 float类型不匹配时不一定报错可能静默截断所以字段长度50要写够。日期用adDBTimeStamp并传DATE结构指针别传字符串。3.3 事务BeginTrans / CommitTrans / RollbackTrans 的正确包法批量写入必须用事务否则一半成功一半失败数据就脏了。ADO 的事务挂在 Connection 上三个方法成对出现。try { m_pConn-BeginTrans(); // 开启事务 // 这里执行多条 INSERT/UPDATE ExecuteInsert1(); ExecuteInsert2(); m_pConn-CommitTrans(); // 全部成功才提交 } catch (_com_error e) { m_pConn-RollbackTrans(); // 任何一步失败就回滚 CString msg (LPCTSTR)e.Description(); AfxMessageBox(msg); }关键点是RollbackTrans必须放在 catch 里且要保证BeginTrans成功后才会走到回滚。如果BeginTrans本身失败回滚会再抛一个错所以更稳的写法是用一个标志位记录事务是否已开启。嵌套事务 ADO 支持有限别在事务里再开事务容易出「不支持嵌套」的错。3.4 错误处理_com_error里到底该看哪个字段ADO 报错信息经常让人摸不着头脑因为_com_error有好几个字段。ErrorMessage()是系统级描述Description()才是 Provider 给的业务描述ErrorInfo里还有原生错误码。catch (_com_error e) { CString msg; msg.Format(_T(Code0x%08X\nDesc%s\nSource%s\nMsg%s), e.Error(), // HRESULT (LPCTSTR)e.Description(), // 最有用的业务描述 (LPCTSTR)e.Source(), // 出错的组件 (LPCTSTR)e.ErrorMessage()); // 系统描述 AfxMessageBox(msg); }排错顺序是先看Description()再看Source()判断是 ADO 还是数据库引擎最后看Error()的 HRESULT。很多「未指定的错误」其实是Description()里有内容但被忽略了。把这段封装成一个函数全项目统一调用能省掉大量重复排查。4. ADO 源码落地避坑5 个血泪踩坑记录4.1 现象程序启动就崩报「CoInitialize 未调用」原因ADO 是 COM 组件任何 ADO 调用前必须初始化 COM。MFC 的AfxOleInit()或CoInitialize(NULL)没调用或者调用顺序在 ADO 之后。解决在InitInstance最前面调AfxOleInit()控制台程序调CoInitialize(NULL)退出时对应CoUninitialize()。多线程里每个线程都要单独初始化别指望主线程初始化了子线程就能用。4.2 现象编译报EOF重定义一堆宏冲突原因#import引入的 ADO 类型库里有个EOF枚举和stdio.h等头文件的EOF宏撞了。解决#import时加rename(EOF, EndOfFile)代码里判断记录集结束用EndOfFile。如果还冲突把#import放在所有标准头文件之后。4.3 现象64 位程序连 Access 报「未找到提供程序」原因Microsoft.Jet.OLEDB.4.0只有 32 位版本64 位进程加载不了。解决要么把工程改成 32 位x86要么装 64 位的Microsoft.ACE.OLEDB.12.0并改连接串 Provider。注意 ACE 的位数也要和进程匹配装了 32 位 ACE 跑 64 位程序一样失败。4.4 现象字段取值偶尔抛异常日志里是「类型不匹配」原因数据库列允许 NULL_variant_t的vt是VT_NULL直接赋给long/double触发转换异常。解决取值前判断if (v.vt VT_NULL) { 用默认值 } else { 正常取 }。字符串字段还要注意VT_NULL和空串是两回事业务上要区分。4.5 现象连接池耗尽程序跑一段时间后连不上原因Connection对象用完没Close()或者异常路径下漏了关闭连接一直占着。解决用 RAII 封装连接析构时自动Close()或者在finally语义的代码块里确保关闭。ADO 连接池默认开启但对象不释放池子也回收不了。排查时看数据库端的连接数比看代码快。5. 进阶把 Ado_Aok_demo 的代码抽成可复用数据访问层demo 跑通只是起点真正省时间的是把它抽成一层薄封装。我的习惯是做一个AdoHelper类构造时接连接串提供ExecuteNonQuery、Query、ExecuteScalar三个方法内部统一处理 COM 初始化检查、异常转换、连接关闭。这样业务代码里看不到_com_error也看不到CreateInstance。class AdoHelper { public: AdoHelper(const CString connStr) { m_pConn.CreateInstance(__uuidof(Connection)); m_pConn-Open(_bstr_t(connStr), , , adConnectUnspecified); } ~AdoHelper() { if (m_pConn m_pConn-State adStateOpen) m_pConn-Close(); } // 返回受影响行数 long ExecuteNonQuery(const CString sql) { _variant_t affected; m_pConn-Execute(_bstr_t(sql), affected, adCmdText); return affected.lVal; } // 返回记录集调用方负责遍历 _RecordsetPtr Query(const CString sql) { _RecordsetPtr rs; rs.CreateInstance(__uuidof(Recordset)); rs-Open(_bstr_t(sql), m_pConn.GetInterfacePtr(), adOpenStatic, adLockReadOnly, adCmdText); return rs; } private: _ConnectionPtr m_pConn; };这个封装的关键取舍是Query返回_RecordsetPtr而不是vector因为字段类型千变万化转成统一结构反而丢信息调用方自己遍历灵活但要注意用完Close()。ExecuteNonQuery用_variant_t接受影响行数lVal取长整型。析构里判断State adStateOpen再关避免重复关闭抛异常。验证这套封装是否可靠我一般做三件事一是用同一个连接连续跑 1000 次查询看内存和连接数是否稳定二是故意传错 SQL确认异常被正确抛出且连接被释放三是在多线程里各建各的AdoHelper确认没有跨线程共享连接。跨线程共享Connection是 ADO 的大忌COM 单元模型会让它时好时坏属于典型的玄学问题别去赌。最后说个我自己的习惯每次接手一个 ADO 老项目第一件事不是读业务代码而是把连接字符串和_com_error处理这两处先看一遍。这两处对了八成问题都能定位这两处糊弄后面全是后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表