
简介这套VC 6.0数据库编程大全的配套代码包面向需要系统掌握MFC数据库开发流程的初学者与进阶开发者围绕ODBC、DAO、OLEDB/ADO等数据库访问技术提供可运行的完整工程示例。压缩包共839个文件、整体仅1.2MB以h头文件与cpp源文件为主兼有rc资源脚本、aps预编译资源、dsp/dsw工程文件、def/mk构建文件与mdb/sql数据库实例目录按ODBC、DAO、OLEDB等主题分模块组织已有339人学习下载。代码覆盖CDatabase/CRecordset的ODBC业务查询、CDaoDatabase/CDaoRecordset的Jet数据库访问、ADO对象封装与调用、数据绑定窗体、多线程并发访问控制、SQL异常捕获与注入防范等实践要点并附SQL脚本与示例库便于验证。对希望从实际代码理解数据库接口差异、快速搭建自己的VC数据库应用的学习者而言这是一份轻量但覆盖面较全的参考素材。1. 老掉牙的 VC6.0 数据库编程这套代码到底能解决什么很多人一看 VC6.0 就觉得是古董但现实是大量工业设备、医院信息系统、高校实验室老项目还在用 MFC 维护招人难、资料少、代码烂。这个“vc6.0 数据库编程大全(代码)”资源包本质是把 MFC 里操作数据库的路子都走了一遍CDatabase CRecordset 的 ODBC 老路、ADO 的 _ConnectionPtr/_RecordsetPtr 新路、DAO 的边角料、以及配套的 SQL 封装和报表绑定代码。适合两类人一类是被临时拉去维护 2005 年前后项目的工程师另一类是毕业设计选了 MFC 数据库、想少走弯路的在校生。这套东西解决的不是“能不能连上”的问题而是“连上之后代码怎么写才不容易崩、改起来才不费劲”的问题。2. 通向数据库的三条老路ODBC、DAO 与 ADO 的选型思考2.1 ODBC 驱动层从配置到连接串VC6.0 时代数据库访问最稳的路子是 ODBC。这里说的 ODBC 不是 MFC 封装而是最底层的 SQLConnect / SQLExecDirect 这一套 API。这套代码在资源包里占了不小比例因为它不依赖 MFC 的运行时机纯 C 语言函数调试起来反而更容易看清问题。// odbc_direct.cpp - 直接用 ODBC API 查询 #include sql.h #include sqlext.h #include stdio.h int main() { SQLHENV henv SQL_NULL_HENV; SQLHDBC hdbc SQL_NULL_HDBC; SQLHSTMT hstmt SQL_NULL_HSTMT; SQLRETURN ret; // 1. 分配环境句柄 ret SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, henv); if (ret ! SQL_SUCCESS) return -1; // 2. 设置 ODBC 版本必须放在分配连接之前 SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); // 3. 分配连接句柄 ret SQLAllocHandle(SQL_HANDLE_DBC, henv, hdbc); // 4. 连接数据源DSN 需要在控制面板里先配好 SQLCHAR connStr[] DSNMyDB;UIDsa;PWD123456;; ret SQLDriverConnect(hdbc, NULL, connStr, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_NOPROMPT); if (ret SQL_SUCCESS || ret SQL_SUCCESS_WITH_INFO) { // 5. 分配语句句柄并执行 SQLAllocHandle(SQL_HANDLE_STMT, hdbc, hstmt); SQLCHAR sql[] SELECT stu_id, stu_name FROM t_student; ret SQLExecDirect(hstmt, sql, SQL_NTS); if (ret SQL_SUCCESS) { // 绑定列 int id; char name[64]; SQLBindCol(hstmt, 1, SQL_C_SLONG, id, 0, NULL); SQLBindCol(hstmt, 2, SQL_C_CHAR, name, sizeof(name), NULL); // 循环取数 while (SQLFetch(hstmt) SQL_SUCCESS) { printf(id%d, name%s\n, id, name); } } SQLFreeHandle(SQL_HANDLE_STMT, hstmt); } // 6. 逆序释放语句 - 连接 - 环境 SQLFreeHandle(SQL_HANDLE_DBC, hdbc); SQLFreeHandle(SQL_HANDLE_ENV, henv); return 0; }这段代码的思路很清晰环境句柄、连接句柄、语句句柄三级递进。环境是全局的整个进程只需要一个连接是每个数据源一条语句是每次查询一条。SQLBindCol 里第二个参数 1 表示列序号从 1 开始不是从 0 开始这是最容易写错的地方。资源包里这段代码的写法是标准教材派但实际上很多老项目不会用这层 API而是用 MFC 封装因为 MFC 帮你管理了大部分句柄生命周期。ODBC API 的优点是移植性强同一套代码在 Windows 和 Linux 上都能编译UnixODBC 环境缺点是写起来啰嗦。如果你只是做 Windows 桌面程序我一般建议直接用 MFC 封装但读一遍这层代码对理解后面所有封装都有帮助——很多诡异报错最后都是回到 ODBC 驱动层查的。2.2 MFC 封装类CRecordset 与 CRecordView 的取舍资源包里的核心代码是 MFC 的 CDatabase 和 CRecordset。这套封装的逻辑是CDatabase 管理连接CRecordset 管理结果集。// RecordsetDemo.cpp - 用 CRecordset 查询 #include stdafx.h #include afxdb.h // 定义一个记录集子类绑定表字段 class CStudentSet : public CRecordset { public: CStudentSet(CDatabase* pDb) : CRecordset(pDb) { m_strSort _T(stu_id); // 默认排序字段 } virtual CString GetDefaultSQL() { return _T(SELECT stu_id, stu_name, stu_score FROM t_student); } void DoFieldExchange(CFieldExchange* pFX) { pFX-SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T(stu_id), m_stuID); RFX_Text(pFX, _T(stu_name), m_stuName); RFX_Double(pFX, _T(stu_score), m_stuScore); } public: long m_stuID; CString m_stuName; double m_stuScore; }; void QueryWithRecordset() { CDatabase db; // OpenEx 比 Open 多一个选项参数可以控制只读 db.OpenEx(_T(DSNMyDB;UIDsa;PWD123456;), CDatabase::noOdbcDialog | CDatabase::readOnly); CStudentSet rs(db); rs.Open(); // 默认 forwardOnly 游标 while (!rs.IsEOF()) { TRACE(_T(%d %s %.2f\n), rs.m_stuID, rs.m_stuName, rs.m_stuScore); rs.MoveNext(); } rs.Close(); db.Close(); }这里有个细节值得注意CRecordset 的游标类型默认是 snapshot快照如果你用的是 dynaset动态集别人改数据你能实时看到但性能开销大。资源包里大部分代码用的默认 snapshot这正好适合报表场景——数据不要求实时但要求稳定一致。CRecordset 和 CRecordView 的配合是 MFC 的一大特色CRecordView 是一个表单视图可以直接把记录集的字段映射到对话框控件上。但我的经验是CRecordView 生成的代码太过绑定界面稍微改一点就得重新映射实际项目中用 CListView 或普通 CDialog 手动绑定的反而更多。资源包里的示例代码两类都有重点看普通对话框那类因为那才是真实项目里能直接搬的。2.3 ADO 路径为什么老项目后来都往这迁移ADO 是 VC6.0 时代晚期的选择很多 2003 年之后启动的项目直接用 ADO 代替了 ODBC。原因是 ADO 的对象模型更接近人类思维连接对象、命令对象、记录集对象三个搞定一切。// AdoDemo.cpp - 用 ADO 连接并查询 #import C:\Program Files\Common Files\System\ado\msado15.dll \ no_namespace rename(EOF, adoEOF) void QueryWithADO() { HRESULT hr ::CoInitialize(NULL); // ADO 需要 COM 环境 _ConnectionPtr pConn; _RecordsetPtr pRs; try { // CreateInstance 是 ADO 的标准初始化方式 hr pConn.CreateInstance(__uuidof(Connection)); hr pConn-Open(_bstr_t(ProviderSQLOLEDB;Data Source127.0.0.1; Initial CatalogStudentDB;User IDsa; Password123456;), _bstr_t(), _bstr_t(), adModeUnknown); // 直接执行 SQL 拿记录集 pRs.CreateInstance(__uuidof(Recordset)); hr pRs-Open(_bstr_t(SELECT stu_id, stu_name FROM t_student), _variant_t((IDispatch*)pConn, true), adOpenForwardOnly, adLockReadOnly, adCmdText); while (!pRs-adoEOF) { _variant_t vId pRs-Fields-GetItem((long)0)-Value; _variant_t vName pRs-Fields-GetItem(_bstr_t(stu_name))-Value; printf(%d %s\n, (long)vId, (const char*)(_bstr_t)vName); pRs-MoveNext(); } pRs-Close(); pConn-Close(); } catch (_com_error e) { // 错误信息里带 SQL 状态码比如 0x80040E14 是语法错误 printf(Error: %s\n, (const char*)e.Description()); } ::CoUninitialize(); }ADO 的坑比 ODBC 更隐蔽。第一个坑是 _variant_t 的类型转换上面代码里 (long)vId 这种强转如果数据库里 stu_id 是 bigint强转 long 会直接丢精度正确做法是判断 vId.vt 再转 _int64。第二个坑是 #import 生成的 msado15.tlh 头文件依赖本机 MDAC 版本换一台机器编译有可能找不到接口解决办法是把 tlh 文件拷到工程目录后改为 include 固定路径。资源包里 ADO 相关的代码量很大覆盖了连接、查询、存储过程调用、事务回滚。如果你要维护的是 2005 年左右的系统ADO 是主力如果是 1998 年以前的系统ODBC 才是主力。两条路都得会看但不需要都精通。3. 把代码落成窗体录入、列表与参数化查询3.1 连接管理别每次查询都 Open/Close看资源包的示例代码很多都是写一个函数里面连接、查询、关闭一条龙。示例没问题但直接搬进项目就翻车因为真实界面是反复操作的——用户点一次查询就关一次连接性能慢还是小事连接数没释放干净会让数据库端的会话数缓慢增长。// ConnectionPoolSimple.cpp - 最简单的连接复用 // 头文件里声明全局连接对象 // extern CDatabase g_db; // 启动时初始化一次 BOOL InitGlobalConnection() { g_db.OpenEx(_T(DSNMyDB;UIDsa;PWD123456;), 0); return TRUE; } // 所有查询都用同一个连接注意只能单线程用 void QueryWithGlobalConnection() { CStudentSet rs(g_db); rs.Open(); while (!rs.IsEOF()) { // 处理记录 rs.MoveNext(); } rs.Close(); }这是最简单的连接复用方案缺点是 MFC 的 CDatabase 不是线程安全的多个线程同时操作同一个 g_db 对象会崩。资源包里的代码默认单线程这是对的——MFC 的 CDialog 应用基本都是 UI 线程里干活耗时查询会卡界面但这是另一个话题。更好的方案是每个线程一个 CDatabase 实例线程结束时统一关闭。我在老项目里改造时就是这么干的用一个 CMap 按线程 ID 存 CDatabase 指针线程退出时回收。这个思路可以照搬进任何维护中的 MFC 工程资源包里的基础代码够你改造成这个模式。3.2 列表控件绑定CListCtrl 的虚拟列表用法数据库查询结果展示到界面上90% 的场景是 CListCtrl。老代码常见做法是循环 InsertItem数据量几百条没问题上万条就开始卡。资源包里有一份虚拟列表的例子值得专门说。// VirtualListDemo.cpp - 虚拟列表绑定 // 原理CListCtrl 不存数据只按需向程序要数据 class CVirtualListCtrl : public CListCtrl { public: // 把记录集数据放到 vector 里 std::vectorCString m_vecNames; std::vectorlong m_vecIDs; void LoadData(CRecordset rs) { rs.MoveFirst(); while (!rs.IsEOF()) { // 假定记录集有两个字段 long id; CString name; // 从 RFX 变量取值 // 这里省略具体字段赋值代码 m_vecIDs.push_back(id); m_vecNames.push_back(name); rs.MoveNext(); } } }; // 消息映射里处理 LVN_GETDISPINFO void OnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult) { LV_DISPINFO* pDispInfo (LV_DISPINFO*)pNMHDR; LV_ITEM* pItem pDispInfo-item; if (pItem-mask LVIF_TEXT) { int row pItem-iItem; switch (pItem-iSubItem) { case 0: // 第0列 lstrcpyn(pItem-pszText, m_vecNames[row], pItem-cchTextMax); break; case 1: // 第1列 wsprintf(pItem-pszText, _T(%ld), m_vecIDs[row]); break; } } *pResult 0; }虚拟列表的关键是 SetItemCount 之后控件发 LVN_GETDISPINFO 消息时才取数据所以滚动再快也不会卡。注意 lstrcpyn 的第三个参数是目标缓冲区大小漏了这个会有溢出风险。资源包里的示例代码把这两步都写全了对照着抄就行。虚拟列表不适合需要原地编辑的场景——编辑还是要靠内嵌编辑框或者干脆用 CListCtrl 的 LVS_EDITLABELS 模式但那会引入消息冲突。我的习惯是只读报表用虚拟列表需要编辑的数据行改用 CListView 配合子类化处理或者干脆分两个控件。3.3 参数化查询拼接字符串是隐患源头老代码里最常见的灾难是把用户输入直接拼进 SQL 字符串。这不仅仅是 SQL 注入的问题还有单引号、百分号等字符导致查询报错的问题。资源包里的示例代码基本都用参数化这是这份资源比网上一堆散装代码值钱的地方。// ParameterQuery.cpp - 用参数化避免注入和格式错误 CRecordset rs(db); // 直接写参数占位符不用拼接 CString sql _T(SELECT * FROM t_student WHERE stu_name LIKE ? AND stu_score ?); rs.Open(CRecordset::snapshot, sql); // 关键参数顺序必须和 SQL 里 ? 顺序一致 rs.SetParamValue(0, _T(%张%)); // stu_name 模糊匹配 rs.SetParamValue(1, (long)60); // stu_score 大于 60 while (!rs.IsEOF()) { // 处理数据 rs.m_strFilter.Empty(); // 注意别混用 m_strFilter 和 SetParamValue rs.MoveNext(); }SetParamValue 的索引从 0 开始这个和 ODBC 的列索引从 1 开始正好相反写的时候要格外注意。另外 m_strFilter 和 SetParamValue 最好不要混用m_strFilter 是拼进 WHERE 子句的比如 _T(stu_name张三)如果值里有单引号就直接爆。我见过一个维护中的系统因为有人把用户输入的姓名直接填到 m_strFilter 里导致输入“OBrien”这种名字时查询必崩改参数化之后彻底解决。参数化之后 SQL 还能不能走索引答案是只要数据库端是预编译SQL Server 里 ? 会被当成参数变量索引利用率高于字符串拼接因为字符串拼接会产生大量不同的 SQL 文本导致计划缓存膨胀。4. 避坑手册VC6.0 数据库编程五个典型翻车现场4.1 现象连不上数据库一直报“未找到数据源名称”刚拿到资源包在自己机器上跑示例时最常见的就是这个。代码没错、驱动没错就是控制面板里的 ODBC 数据源DSN没配。原因ODBC 和 ADO 的 ProviderSQLOLEDB 不同。ODBC 的 DSN 分为用户 DSN 和系统 DSN程序以服务方式运行的时候只能识别系统 DSN而且 64 位系统上 ODBC 管理器有两个版本VC6.0 是 32 位程序必须用 32 位的 odbcad32.exe 配置。解决先确认 VC6.0 程序是 32 位然后打开 C:\Windows\SysWOW64\odbcad32.exe 建系统 DSN。注意 SQL Server 的 Native Client 版本也要匹配——老机器上装的是 SQL Server 2000 驱动新机器上只有 ODBC Driver 17 for SQL Server连接字符串写法完全不同。提示调试时先用记事本建一个 .udl 文件双击测试连接串能不能通比在代码里反复试错快得多。4.2 现象程序编译通过一运行就崩溃VC6.0 的工程跑起来直接崩最常见的是 _com_error 没接住或者 CRecordset 的指针没判空。原因MFC 的 CRecordset 在 Open() 失败时会抛异常CDBException如果你没把 Open 包在 try/catch 里程序会直接弹一个“应用程序错误”然后退出。另一个原因是多字节字符集下CString 传给 SQL 的地方用了 TCHAR 数组长度不够导致栈溢出。解决所有数据库操作包一层 try/catch至少要知道异常类型是 CDBException 还是 _com_error。代码层面Open 之前先判指针if ((CDatabase*)db db.IsOpen()) { // 连接是好的继续 } else { AfxMessageBox(_T(数据库未连接)); }那段排错代码只是一个防御真正要改的是把 Open 和操作分开Open 只做一次操作处只判断 IsOpen。资源包里的示例代码都把 Open 写在构造函数里这在真实项目里是坏味道。4.3 现象中文乱码插入和查询结果不一样数据库里存的中文读出来变成“???”或者读出来是对的写进数据库就乱。这是 VC6.0 时代的头号玄学问题。原因VC6.0 默认的工程环境是 MBCS多字节字符集而 SQL Server 的字段排序规则又是 Chinese_PRC_CI_AS。两个体系不一致时IP 地址连接和命名管道连接走不同的代码页转换结果就乱了。还有一个常见来源在 ANSI 字符串和 UNICODE 字符串之间互转时用了错误的转换函数。解决统一用 _T() 宏 CString而且 CRecordset 的 RFX_Text 有个长度参数默认是 255如果字段是 varchar(500)RFX_Text 第二个参数必须写成 500否则截断后可能产生半个汉字显示为乱码。4.4 现象表名或字段名是关键字SQL 一直报语法错误示例代码里都用 stu_id、stu_name 这种规矩命名一换到真实系统就出问题。比如字段名叫 name、order、group、descSQL Server 直接报“Incorrect syntax near the keyword order”。原因老系统的表结构是十几年前建的那时候命名不规范关键字当字段名的情况很常见。直接拼 SQL 必然撞语法错误。解决给表名、字段名加方括号[order] [group] 这样。用 ODBC 的话在 GetDefaultSQL 里写return _T(SELECT [name], [order] FROM [t_user] WHERE [group] A);这不是你代码的问题是表结构本身就埋了雷。接手老项目时先把所有字段名导出来写个小工具自动检测有没有关键字能省很多晚上的时间。4.5 现象界面假死点一下卡几秒用 MFC 直接在主线程里执行数据库查询数据量一大界面就僵住。原因CDatabase::OpenEx 和 CRecordset::Open 都是阻塞调用执行期间 UI 线程的消息循环被卡住鼠标和键盘事件全部排队。SQL Server 端执行一个慢查询要 5 秒钟界面就死 5 秒。解决慢查询放到工作线程。注意 CRecordset 不能跨线程使用每个线程必须自己创建 CDatabase 实例。常用的改造模式是 PostMessage 通知 UI 线程接收结果把记录集的指针保护起来。资源包里的示例代码没有完整的多线程版本但我的经验是即使不加多线程先把连接改成全局单例 查询结果用 vector 缓存界面卡顿都能减轻不少因为反复建连接才是最大的性能黑洞。5. 事务控制与 SQL 语句调试最后一段提升5.1 事务边界把一组操作包起来老系统里常见的严重问题是往主表插入一条记录然后往明细表插几条子记录中间出了错主表有数据明细表缺了几行。排查的时候数据口径对不上骂了半天才知道是代码里没写事务。// TransactionDemo.cpp - 用 CDatabase 管理事务 void InsertOrder(CDatabase db, long orderID, CString customer) { db.BeginTrans(); // 开启事务 try { // 第一步插入主表 CString sqlMain; sqlMain.Format(_T(INSERT INTO t_order (order_id, customer) VALUES (%ld, %s)), orderID, customer); db.ExecuteSQL(sqlMain); // 第二步插入明细表此处省略循环 CString sqlDetail; sqlDetail.Format(_T(INSERT INTO t_order_detail (order_id, item) VALUES (%ld, item01)), orderID); db.ExecuteSQL(sqlDetail); db.CommitTrans(); // 都成功才提交 } catch (CDBException* e) { db.RollbackTrans(); // 任何一步失败都回滚 e-Delete(); throw; // 重新抛出让上层处理 } }事务在 ODBC 下的行为容易让人误解BeginTrans 之后数据不一定实际锁在数据库端取决于驱动和数据库的默认隔离级别。SQL Server 出厂默认是 Read Committed所以 BeginTrans 期间的读操作看到的还是老数据这点和 Oracle 的默认行为不同跨库调试时容易踩。另一个注意点CDatabase 的事务和 CRecordset 的事务是绑在一起的。如果你在一个事务里用两个 CRecordset 对象其中一个 Open 失败整个事务回滚之前已经成功的记录也会消失。资源包里的示例代码把事务封装成了独立函数这是好习惯——事务的粒度最好控制在函数级别不要横跨多个函数调用否则哪一步没走完根本理不清。5.2 用日志函数把 SQL 打印出来维护老项目最痛苦的事是“这个 SQL 到底是哪行代码生成的”。MFC 的 TRACE 宏只在调试版本输出发布版本根本没有信息。我习惯在每个数据库操作函数里加一个日志开关// SqlLogHelper.cpp void LogSql(const CString sql, const CString tag) { #ifdef _DEBUG // 调试版输出完整 SQL 到输出窗口 TRACE(_T([%s] %s\n), tag, sql); #else // 发布版写到一个简单的文本文件 FILE* f fopen(sql_log.txt, a); if (f) { fprintf(f, [%s] %s\n, (LPCTSTR)tag, (LPCTSTR)sql); fclose(f); } #endif }配合上面的事务代码调用时在每条 ExecuteSQL 之前加一行 LogSql可以把最终执行的 SQL 完整记录下来。这个习惯帮我解决过很多次“测试环境正常、生产环境数据不对”的诡异问题——把两边的 SQL 日志一对比立刻发现是生产环境的表结构多了一个字段旧代码没带上这个字段。这看起来是个土办法但是在 VC6.0 这种没有自带 ORM、没有 SQL 拦截器的环境里这几乎是唯一可靠的排查手段。从那以后我每次接老项目的第一件事就是给数据库访问层加上这个日志开关而不是急着改代码。日志能让你看见程序实际发给数据库的语句而不是你以为的语句。希望帮到你。本文还有配套的精品资源点击获取