简介:一套基于MFC与ODBC的MySQL数据访问实例源码包,面向需要在VC++环境下开展桌面数据库开发的初学者和中级开发者。资源展示了通过ODBC接口连接MySQL、执行SQL查询、处理结果集以及关闭连接的完整流程,可直接作为课程设计或毕业设计的参考模板。压缩包约一百八十一KB,共一百一十七个文件,以四十九个C源文件与四十六个头文件为主体,另含VS工程配置、makefile及说明文档,整体结构清晰,便于直接编译和查阅。已有145人学习浏览过该资源。包内代码覆盖数据库连接建立、记录集操作、异常处理与资源释放等关键环节,适合希望理解MFC中CDaoDatabase和CDaoRecordset实际用法的读者。从连接初始化到结果集遍历,均有可参考的实现细节,能帮助减少重复踩坑,快速掌握ODBC访问数据库的常见写法。
1. 一个MFC连MySQL的老工程:先搞清资源里装的是驱动还是案例
做桌面数据库工具的老工程师,手里大概率存过这种压缩包:文件名是“MySQL.rar_MFC MYSQL_MFC Mysql_MySQL MFC c_MySql ODBC_odbc mys”,解压出来不是一整个工程,而是myodbc.001、mysys.001、strings.001、lib.001等分卷文件,外加dbug.c、_ctype.c、libmysql.c、results.c、info.c、catalog.c这些C源码。这不是一个简单的MFC示例,而是把MySQL的ODBC客户端库源码、C API库源码和一个基于ODBC的MFC访问案例打包在了一起。它能解决的问题,是让一个老式VC++程序绕过新版驱动配置,直接通过ODBC标准接口读写MySQL数据;适合正在维护十年前MFC项目、或需要在不装完整IDE的环境下复现数据库链路的开发者。先说结论:里面真正能直接用的,是ODBC数据源的连接思路和文件清单里的库文件名,源码本身需要在新编译器下做不少调整。
2. MFC侧ODBC编程:从配置DSN到CDatabase查询的完整路径
2.1 选CDatabase而不是CDaoDatabase:这个坑从摘要就开始
资料里写“MFC中的ODBC编程主要依赖于CDaoDatabase和CDaoRecordset”,这是错误的。CDaoDatabase和CDaoRecordset属于DAO(Data Access Objects),走的是Jet引擎访问Access那条路线;真正对应ODBC标准的是CDatabase与CRecordset。新建MFC项目时,如果勾选了“支持ODBC”,生成的CWinApp派生类里会看到AfxDbInitModule()初始化调用,这个函数专门用来加载MFC的ODBC动态库,跟DAO没有任何关系。
如果照着错误资料用CDaoDatabase去连MySQL ODBC数据源,最常见的现象是编译能通过,但运行时Open连接直接抛异常,或者提示找不到DAO引擎。因为DAO访问MySQL不是完全不行,但需要额外桥接层,稳定性很差。正确做法是在stdafx.h里包含afxdb.h(ODBC类)而不是afxdao.h。文件里的案例如果用了CDaoDatabase,第一件事就是把它替换成CDatabase,否则后面所有步骤都会翻车。
2.2 配置MySQL ODBC数据源:32位和64位别搞错
不管手头是Connector/ODBC 5.x还是8.x,配置DSN的入口要选对。Windows 10/11上odbcad32.exe有SysWOW64和System32两份,前者管32位程序,后者管64位程序。MFC工程默认Win32生成时,必须用C:\Windows\SysWOW64\odbcad32.exe配置,用错入口会导致“找不到数据源”。操作步骤通常是:打开对应位数的ODBC数据源管理器,在“系统DSN”或“用户DSN”里添加MySQL ODBC驱动,填写Data Source Name、TCP/IP Server、Port、Database、User和Password。
配置完可以用“测试”按钮验证。注意MySQL 8.0默认认证插件是caching_sha2_password,老版本Connector/ODBC只支持mysql_native_password,测试时如果报“Authentication plugin 'caching_sha2_password' cannot be loaded”,要么改MySQL用户认证插件,要么升级驱动到8.0.x。这一步是MFC连MySQL最常见的暗坑,后面专门讲。
2.3 用CDatabase打开连接并执行CRecordset查询
DSN配好之后,MFC侧代码就标准化了。下面这段是我常用的最小模板,放在对话框按钮事件里就能跑通查询。
// 头文件里包含 #include <afxdb.h> // 建立数据库连接 CDatabase db; // 注意:第三个参数和第四个参数传NULL,连接串直接写在OpenEx里更灵活 db.OpenEx(_T("DSN=MySqlDSN;UID=root;PWD=123456;"), CDatabase::noOdbcDialog); // 创建记录集并执行SQL CRecordset rs(&db); // 打开记录集,查询所有用户名和备注 rs.Open(CRecordset::forwardOnly, _T("SELECT username, note FROM userinfo")); // 遍历结果集 while (!rs.IsEOF()) { CString strUser; CString strNote; // GetFieldValue按列索引取,0是第一列 rs.GetFieldValue((short)0, strUser); rs.GetFieldValue((short)1, strNote); // 这里做你的业务处理 TRACE(_T("%s - %s\n"), strUser, strNote); // 移到下一条记录 rs.MoveNext(); } // CRecordset析构时会自动关闭记录集,但显式关闭更稳妥 rs.Close(); db.Close();逻辑说明:OpenEx的第一个参数是完整连接字符串,里面直接写DSN名、用户名和密码,第三个参数传noOdbcDialog是为了禁止程序运行到一半弹ODBC配置框;CRecordset构造函数需要传入CDatabase指针,这样记录集复用同一条连接。forwardOnly类型只允许向前遍历,适合只读的批量查询,性能开销最小。GetFieldValue第一个参数是列的索引从0开始,第二个是输出用的CString,它重载支持按SQL类型自动转换,拿到数字字段时也能转成字符串。
如果你要执行INSERT、UPDATE这类不返回结果集的SQL,不要走CRecordset,直接用db.ExecuteSQL()更干净。例如更新操作:
CString strSQL; strSQL.Format(_T("UPDATE userinfo SET note = _T('%s') WHERE username = _T('%s')"), strNewNote, strUser); // 执行更新 db.ExecuteSQL(strSQL);这里要注意:ExecuteSQL是同步阻塞的,数据量大时界面会卡,建议包一层线程;另外SQL里带单引号时需要转义成两个单引号,否则会把SQL截断。
2.4 连接串的参数拆解与动态拼接
连接串格式虽然简单,但值得拆开看,因为在不同环境里复现失败基本都是参数问题。标准格式是:
DSN=MySqlDSN;UID=root;PWD=123456;SERVER=127.0.0.1;PORT=3306;这里DSN是ODBC数据源名称,UID和PWD对应MySQL账号,SERVER和PORT可以写在DSN里,也可以写在连接串里覆盖。使用连接串时有个小习惯:不要把所有信息写死在代码里,用配置模块动态拼接。常见做法是用CString::Format读取ini文件里的服务器地址、端口、账号、密码,这样换环境时不用重新编译。
另一个容易踩的是MySQL 8.0驱动要求明确TCP连接参数,否则可能走默认socket连接。所以连接串里最好显式写SERVER=127.0.0.1;PORT=3306;,避免解析到本机socket。驱动版本是5.x时,SERVER参数不支持,直接靠DSN内部配置,代码里不传反而更稳。我的习惯是准备两套连接串模板,根据驱动版本动态切换。
3. 解开资源里的源码分卷:myodbc.001到catalog.c到底有什么用
3.1 文件清单里那些.001分卷和.c文件的角色
压缩包里的myodbc.001、mysys.001、strings.001、lib.001不是普通数据文件,而是老式分卷压缩包的分卷编号。这种命名方式常见于早期RAR分卷上传,下载时按顺序放到同一目录,用WinRAR打开第一个分卷就能还原完整包。还原后的内容大致对应MySQL早期版本的源码树片段:myodbc是MySQL ODBC驱动的前身源码,mysys是MySQL自己的基础系统库(内存分配、文件句柄、错误处理等),strings是字符与字符串处理函数集,lib则是客户端库源码。dbug.c是那个时代有名的调试跟踪库(malloc和free的封装、断言跟踪),_ctype.c是字符集判断与转换的C实现,libmysql.c是MySQL官方C API的核心实现,results.c、info.c、catalog.c则分别是结果集处理、连接信息查询和目录元数据访问的源文件。
这些文件说明这个资源比单独的“MFC调MySQL”案例更底层:它把MySQL C API和ODBC驱动源码一并塞了进来。对于一个需要定制驱动行为或研究内部协议的工程师来说,它是学习材料;但如果你只是要跑通MFC界面读数据,这些源码不需要编译,放着不看即可。
3.2 老C源码在新编译器下的编译取舍
真要去编译这些老C文件,会遇到一堆版本问题。当时的内库代码大量使用未加下划线前缀的内部函数,在VS2015以上版本里会被_CRT_SECURE_NO_WARNINGS拦截,报strcpy、sprintf这类警告或错误。处理方式有三种。
第一,工程预处理器里加上_CRT_SECURE_NO_WARNINGS和_CRT_NONSTDC_NO_DEPRECATE,屏蔽掉弃用告警。第二,打开/D "SQL_WCHART_CONVERT",让驱动代码兼容宽字符编译。第三,也是最现实的:不要直接编译源码里的MySQL库,把libmysql.dll和libmysql.lib用现成的发行版。编译这些C源码涉及MySQL内部的线程模型、字符集表生成脚本和平台相关宏,在新系统上重建一遍耗时且易错,除非你就是要定制ODBC驱动,否则性价比太低。
如果必须编译,建议先跑一遍网上的“老MySQL源码迁移VS2017”清单:撤销my_global.h里的过时宏、补充stdint.h、把pthread_*调用替换成Windows的_beginthreadex。这些改动量不大,但对底层宏定义的理解要求不低。
3.3 用官方Connector/ODBC替代源码的路线
零基础复现这个资源时,最稳妥的路线不是去碰源码,而是直接用官方驱动。到MySQL官网下载Connector/ODBC 8.0安装包,安装时勾选“Install ODBC Driver”,然后回到ODBC数据源管理器确认驱动出现在列表里。这个驱动同时支持MySQL 5.7和8.0,认证协议兼容性也远好于老驱动。资源里的myodbc源码主要用于学习或排查老驱动行为,不参与实际编译。
判断一个ODBC链接是否走通了,有个简单方法:在ODBC数据源管理器里点“配置”然后“测试”,如果测试通过,MFC侧代码基本不会出大问题。如果测试失败,先解决驱动与MySQL服务器之间的网络、端口和认证问题,再回到代码层面调试,不要反过来。我见过太多人花一整天调MFC代码,最后发现是DSN对应的驱动是32位,而程序是64位这种低级错位。
4. 避坑:MFC+ODBC+MySQL最常见的五个报错与排查
4.1 现象:连接报“找不到数据源”但DSN明明配好了
DSN在ODBC数据源管理器里能看到,程序运行时报SQLState: IM002或[Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序。原因几乎都是位数不匹配:MFC工程编译成Win32,但DSN配在了64位ODBC管理器里。Windows的odbcad32.exe有两个,32位程序必须读SysWOW64里的配置。解决方法是打开C:\Windows\SysWOW64\odbcad32.exe重新配置相同名称的DSN。另外检查工程属性——如果“平台”是x64,DSN必须配在System32的ODBC管理器里。这个坑是重复率最高的,排查优先级永远放在第一位。
4.2 现象:编译阶段CDaoDatabase声明不明确或无法解析
照着资料写代码时,如果#include <afxdao.h>和#include <afxdb.h>都在,会出现“CDaoDatabase::Open”与“CDatabase::OpenEx”参数不匹配这类混淆。原因是DAO与ODBC的类名在某些头文件里都有定义,编译器在查找时产生了多义性。解决方法是只保留afxdb.h,并删除所有CDao*相关代码。如果工程里其他模块确实用了DAO(比如访问Access mdb),就把ODBC和DAO分离到两个不同的类文件,不要让它们在同一个cpp里互相可见。链接阶段报unresolved external symbol "public: virtual __thiscall CDatabase::~CDatabase(void)",则是没在stdafx.h里加上#include <afxdb.h>,或没勾选“使用MFC”而选了“使用共享DLL中的MFC”。检查项目配置里的“MFC的使用”,改成“在共享DLL中使用MFC”即可。
4.3 现象:MySQL 8.0认证插件不兼容导致连接失败
MySQL 8.0默认创建的用户使用caching_sha2_password,而Connector/ODBC 5.3及以下版本只支持mysql_native_password。现象是ODBC测试连接报“Authentication plugin 'caching_sha2_password' cannot be loaded: 无法加载”。两种解决路径:一是把驱动升级到8.0.x,新版完全支持caching_sha2_password;二是把现有用户改成老插件,执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;。注意改了插件会影响密码校验策略,生产环境谨慎操作。如果是远程连接,还要核对该用户允许的主机范围不是localhost,否则会报“Host not allowed”。
4.4 现象:从MySQL读出的中文是乱码
查询结果里的中文在界面上显示为??或锟斤拷。原因不是MFC的Unicode问题,而是连接的字符集没对齐。MySQL服务器、ODBC驱动、客户端三方字符集必须一致。在OpenEx成功之后,先执行一句db.ExecuteSQL(_T("SET NAMES utf8mb4"));,再创建记录集。注意如果数据库表本身是gbk,这里就改成SET NAMES gbk。另外MFC工程如果是Unicode字符集,CString内部是UTF-16,GetFieldValue拿到CString后要确认它走的是ODBC的宽字符转换,一般不会有问题;如果还乱,改用CDatabase::m_hdbc和SQLGetData直接取宽字节。乱码问题有九成是SET NAMES没执行,先补上这行再说。
4.5 现象:存储过程或含多条SQL的记录集打开失败
CRecordset::Open执行一条带多个结果集的批SQL,或者调用MySQL存储过程时,报[MySQL][ODBC Driver] Lost connection during query或结果集计数错误。原因是MySQL协议允许一个查询返回多个结果集,而CRecordset设计上只处理第一个结果集,第二、第三个结果集会让ODBC驱动挂在等待状态。解决方式是改用CDatabase的ExecuteSQL执行存储过程,或者用SQLMoreResults循环获取剩余结果集。我的建议是存储过程这类逻辑直接放服务端,MFC端只调用ExecuteSQL,避免在客户端解析多个结果集。如果必须处理多结果集,用原生ODBC API写好释放逻辑,不要硬套CRecordset。
5. 进阶:把案例模板改造成一个可复用的MFC MySQL查询工具
5.1 封装CMySqlDB基类和错误处理
老案例里的代码都是散在按钮事件里,换一张表就要复制粘贴。更好的做法是封装一个CMySqlDB类,把连接生命周期管起来。
class CMySqlDB : public CDatabase { public: // 打开连接,strDsn是DSN名,strUser/strPwd是账号密码,bDlg为false禁止弹窗 bool Connect(const CString& strDsn, const CString& strUser, const CString& strPwd) { CString strConn; strConn.Format(_T("DSN=%s;UID=%s;PWD=%s;SERVER=127.0.0.1;PORT=3306;"), strDsn, strUser, strPwd); try { OpenEx(strConn, CDatabase::noOdbcDialog); } catch (CDBException* e) { // 把错误信息取出来 CString strErr = e->m_strError; e->Delete(); AfxMessageBox(strErr); return FALSE; } return TRUE; } // 执行无结果集SQL,返回受影响行数 void ExecSQL(const CString& strSQL) { ExecuteSQL(strSQL); } // 释放连接,析构时自动调用 void Disconnect() { if (IsOpen()) Close(); } };逻辑说明:类继承自CDatabase,Connect内部把DSN、账号、密码拼进连接串,OpenEx抛出CDBException时用e->m_strError取详细错误,Delete()释放异常对象,避免内存泄漏。ExecSQL直接转发给基类ExecuteSQL。析构时如果连接还开着就关闭,这样调用方忘记调用Disconnect也不会悬挂数据库连接。参数说明:strConn里显式写了SERVER=127.0.0.1,如果DSN已经配好IP地址,这行可删;PORT=3306需要和MySQL实际端口一致。
5.2 用ODBC Trace验证连接与查询状态
当代码走到OpenEx成功,但查询行为不符合预期时,不要回代码里到处加MessageBox,打开ODBC驱动自带的跟踪日志会更客观。在ODBC数据源管理器“跟踪”选项卡里勾选“启动跟踪”,日志路径设为C:\odbc_trace.log,然后重新运行程序。日志会记录每次SQLAllocHandle、SQLDriverConnect、SQLExecDirect的返回值与状态,能直接看出来是连接串解析失败还是SQL语句被截断。我排过一个问题:SET NAMES utf8mb4一直执行失败,跟踪日志显示该语句还没发送,因为OpenEx返回的句柄在记录集打开前没真正绑定。加了SQLGetInfo接受到连接就绪状态后再执行SET语句,问题瞬间定位。
5.3 给新手的验证路径与MFC消息机制结合
把这个类放进对话框工程后,建议先用一个最简单的单表查询验证:新建对话框,放一个CListCtrl,按钮里调用CMySqlDB::Connect并执行SELECT * FROM userinfo,把结果填进列表。跑通后再去加参数查询、事务和线程化。在线程里操作CDatabase时,记住CDatabase对象不能跨线程直接共享,一个连接只能被创建它的线程使用,需要在线程函数内重新Connect。如果在MFC多线程界面下打开记录集,界面刷新要等查询完成,耗时查询建议用AfxBeginThread,查询结果通过PostMessage回到主线程再更新控件。
从那以后我每次把案例代码迁移到新工程,都强制走一遍这条验证路径:先配DSN,再测试ODBC,然后写最小查询,最后才封装类。这套流程让我少走了很多弯路,也希望能帮到你。
本文还有配套的精品资源,点击获取