简介:一份面向C++程序员的MySQL数据库访问示例工程,基于微软基础类库与开放数据库互连技术,目标是帮助需要在Windows桌面应用中集成数据库功能的开发者理清整体实现思路。资源围绕开放数据库互连接口完整演示了从环境准备、连接建立、查询执行到结果集遍历的流程,压缩包共117个文件,约181KB,主体为49个C源码文件与46个头文件,另有工程文件、构建脚本和说明文档,方便直接打开对照学习。目前已有145人学习下载,虽属轻量级示例,但参考价值比较集中。这份示例的核心价值在于提供了一套可以复用的代码框架,其中既有初始化与数据源配置部分,也涉及与MySQL驱动相关的底层调用,能帮助读者理解应用程序与数据库交互时的数据流走向。通过阅读和二次修改,可以省去从零搭环境的时间,快速落地自己的数据库功能模块。
1. MFC连接MySQL为什么先别急着写代码:ODBC与直连API的选型逻辑
接手一批老项目的维护,或者自己从零搭一个Windows桌面工具,最常碰到的组合就是MFC + MySQL。这个搭配本身不复杂,真正让人头疼的往往是连库方式没选对,写着写着就被各种玄学问题缠住。标题里同时出现了ODBC和c_MySql,恰好就是两条主流路线的代表:MFC原生封装的CDatabase走ODBC驱动,另一种是直接用MySQL官方C API。两条路各有各的适用场景,选错了后面每一步都是坑。
如果你要连接的是公司内网的MySQL 5.7/8.0,数据量不大、增删改查为主,用MFC自带的CDatabase + CRecordset配ODBC数据源是最稳妥的方案,省去自己管理连接句柄的麻烦。但如果你的程序要跑批量写入、事务控制、或者需要在多线程里并发查询,ODBC封装反而碍手碍脚,此时直接调MySQL C API更可控。这篇文章从环境配置讲到代码落地,最后给出一个我常用的封装套路,照着做基本能绕开大部分翻车点。
2. 先把环境跑通:安装MySQL并配置ODBC数据源
2.1 MySQL服务端安装:版本选择和初始账号的坑
无论你是从mysql下载官网拿安装包,还是用压缩包解压版,MySQL 8.0和5.7的初始配置逻辑差别很大。这里强烈建议新项目直接上8.0,但有一个容易踩坑的地方:8.0默认的caching_sha2_password认证插件,在MFC的ODBC驱动下偶尔会抽风——表现是DSN测试连接成功,但程序里CRecordset.Open直接报“Access denied”。
常见做法是安装时把认证插件改回mysql_native_password,或者安装完成后执行一条SQL把root账号的插件切回去:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;这段SQL的逻辑是把root的认证方式从默认的caching_sha2_password改回旧的mysql_native_password。原因是MFC这边的ODBC驱动版本如果低于8.0.26,对caching_sha2_password的支持不稳定,会在握手阶段失败。注意执行完必须FLUSH PRIVILEGES让改动立即生效,不然下次重连还会报同样的错。如果你用的是5.7,这一步可以跳过,但建议还是顺手执行,统一兼容性。
2.2 安装并配置MySQL ODBC驱动
装完服务端还要装驱动,这一步很多人会漏。去MySQL官网下载对应位数的ODBC Driver,需要注意一个关键点:你的MFC程序编译成x64就必须装64位驱动,编译成x86就装32位驱动,两者混用会直接报“找不到数据源”。判断方法是打开ODBC数据源管理器——运行odbcad32.exe看到的是32位,运行C:\Windows\System32\odbcad32.exe是64位,区分清楚再配置。
配置DSN时,名称建议全英文不要带空格,比如直接用mysql_mfc,因为MFC的CDatabase在连接时对DSN名解析比较死板,带空格或中文的DSN偶尔会莫名失败。TCP/IP Server填127.0.0.1,Port填3306,Database选你实际要操作的库。做完后点Test按钮验证连通性,这一步通则后面代码基本没大问题。
打开ODBC数据源管理器后注意:用户DSN和系统DSN的区别在于权限范围。如果你的MFC程序以普通用户启动,用用户DSN;如果程序要以管理员权限运行(比如要写C盘下文件),用系统DSN更保险,否则会出现“找不到DSN”这种看起来毫无道理的报错。
2.3 最小可用的MFC连接代码
配好DSN后先写一段最小代码验证通路,不要急着封装:
#include "stdafx.h" #include <afxdb.h> CDatabase db; CString strConn; strConn.Format(_T("DSN=mysql_mfc;UID=root;PWD=123456;")); BOOL bOk = db.OpenEx(strConn, CDatabase::noOdbcDialog); if (!bOk) { AfxMessageBox(_T("连接失败")); return; } AfxMessageBox(_T("连接成功")); db.Close();OpenEx的第一个参数是连接字符串,DSN指向刚才创建的ODBC数据源,UID和PWD是MySQL账号密码。第二个参数传CDatabase::noOdbcDialog,意思是禁止弹出ODBC连接对话框——如果不传这个,程序跑起来会在连库前弹一个系统对话框,非常影响自动化运行。打开连接后先Close释放资源,这只是验路,正式代码不建议裸用CDatabase,后面会讲封装方式。
设置里还有一个容易被忽视的参数:连接超时。默认情况MySQL ODBC驱动等待连接的时间可能很长,服务器不通时界面会卡住十几秒。可以通过db.SetLoginTimeout(3)把登录超时压到3秒,注意这条语句要写在OpenEx之前才生效。
3. 用CDatabase和CRecordset做增删改查:核心实现与参数细节
3.1 CRecordset的两种打开模式:snapshot和dynaset
ODBC路径下,CRecordset打开时有一个选项决定数据集的行为方式:快照(snapshot)和动态集(dynaset)。快照模式把查询结果一次性拉到本地,数据量小时性能好,但别人修改数据库内容你这边感知不到;动态集模式实时同步数据库变化,但代价是网络开销大。桌面管理工具一般用快照,后台服务型程序用动态集。
CRecordset rs(&db); CString strSQL; strSQL = _T("SELECT id, name, age FROM user_info WHERE age > 20"); rs.Open(CRecordset::snapshot, strSQL, CRecordset::readOnly); while (!rs.IsEOF()) { CString strName; int nAge = 0; rs.GetFieldValue(_T("name"), strName); rs.GetFieldValue(_T("age"), nAge); // 处理行数据 rs.MoveNext(); } rs.Close();GetFieldValue是取字段值的核心函数,第一个参数是列名(不区分大小写),第二个参数传对应类型的变量引用。注意:取字符串用CString,取整数用long或int。这里有个隐蔽坑:如果SQL查询的某列是NULL,GetFieldValue会失败并抛异常,所以要么在SQL里用IFNULL包一层,要么在调用前用rs.IsFieldNull(&rs.GetFieldValue(...))做判断——后者的用法比较绕,我一般直接改SQL保证不返回NULL。
3.2 写操作:用CDatabase的ExecuteSQL还是CRecordset的AddNew
插入、更新、删除这三种写操作,社区里最常见的是直接调用CDatabase::ExecuteSQL拼接SQL字符串。这招简单直接,但有一个致命问题:SQL注入。如果SQL里的参数来自用户输入,必须老老实实做转义,或者改走参数化查询。ODBC下CRecordset也支持AddNew/Update/Delete这样面向行的操作,天然避免注入,代价是代码量变大、多一次网络往返。
// 方式一:ExecuteSQL直接执行 CString strSQL; strSQL.Format(_T("INSERT INTO user_info (name, age) VALUES ('%s', %d)"), strName, nAge); db.ExecuteSQL(strSQL); // 方式二:CRecordset参数化方式 CRecordset rs(&db); rs.Open(CRecordset::dynaset, _T("SELECT * FROM user_info WHERE 1=0")); rs.AddNew(); rs.SetFieldValue(_T("name"), strName); rs.SetFieldValue(_T("age"), nAge); rs.Update();方式一的便捷性劝很多人直接用,但我实际做过一个后台批量导入工具,用户上传Excel后由程序拼SQL,3万行数据跑下来发现拼串的转义逻辑里有一个边界情况没处理,导致某条含单引号的数据入库失败。后来改成方式二后问题彻底消失。如果你的写入频率不高、数据是自己程序生成的,方式一完全够用;只要数据可能来自用户输入,请直接上方式二。
3.3 事务控制:批量写入不踩坑的基础
MFC里事务控制通过CDatabase的三兄弟实现:BeginTrans、CommitTrans、RollbackTrans。这个机制解决的是“一批操作要么全部成功要么全部失败”的问题,典型场景是批量导入:1000条记录里第500条主键冲突,如果不做事务,前面499条已经落库了,数据处于半完成状态。
db.BeginTrans(); try { for (int i = 0; i < 1000; i++) { // 执行插入 } db.CommitTrans(); } catch (CDBException* e) { db.RollbackTrans(); e->Delete(); }注意两点:BeginTrans之后不要执行任何DDL语句(CREATE TABLE、ALTER TABLE这种),部分MySQL版本下会造成隐式提交,事务就废了;另外事务期间保持连接不关闭是基本要求,CDatabase对象必须一直存活。有个经验值是:单事务里写操作控制在1万条以内,太大时MySQL的undo log会膨胀,执行时间反而变慢,这时候应该分批提交,每批5000条左右。
3.4 中文乱码问题:字符集的三个设置点
MFC + MySQL的中文乱码是最高频的搜索词之一,原因基本落在三个层面。第一层是MySQL服务端的character_set_server配置,第二层是ODBC连接自身的字符集设置,第三层是MFC程序里的字符集(Unicode还是多字节)。三层只要有一层不一致,中文就花。
我的固定做法是:MySQL服务端在my.ini里写死character-set-server=utf8mb4,ODBC DSN的高级选项里把Character Set设成utf8mb4,MFC程序工程属性里统一用Unicode字符集。三层全是utf8mb4之后,乱码基本绝迹。注意一个特例:如果数据库里已有存量数据是gbk编码的,你强行把连接字符集改成utf8mb4,读出来的老数据照样乱——这种场景只能转换存量数据,或者连接用gbk去适配老库。
4. 绕开ODBC直连MySQL C API:什么时候值得这么做
4.1 为什么有时候ODBC反而不香了
ODBC的优势是MFC天然支持,API简单,劣势是中间隔了一层驱动管理器,控制力受限。具体问题表现:长时间运行的批量写任务偶发卡死、难以准确获取MySQL本身的错误码、做存储过程调用时参数绑定的文档说明不完整。遇到这类场景,直接连MySQL官方C API反而更省事。官方库提供mysql_init/mysql_real_connect/mysql_query一组原生接口,错误处理更透明,对存储过程和事务的控制也更精确。
用C API的前提是先准备好开发库:mysql-connector-c或mysql-connector-c++,在工程属性里配置好include目录和lib目录。常见的做法是编译时链接libmysql.lib,运行时把libmysql.dll放到exe同目录。dll版本一定要和编译时的lib版本一致,否则启动直接报“找不到指定的模块”或者“应用程序无法启动”。
4.2 C API连接与查询的最小实现
#include <mysql.h> MYSQL* pConn = mysql_init(NULL); if (pConn == NULL) { // 内存不足,初始化失败 return FALSE; } unsigned int nTimeout = 3; mysql_options(pConn, MYSQL_OPT_CONNECT_TIMEOUT, &nTimeout); MYSQL* pRet = mysql_real_connect( pConn, // 连接句柄 "127.0.0.1", // 主机名 "root", // 用户名 "123456", // 密码 "mydb", // 数据库名 3306, // 端口 NULL, // 默认socket,Windows下传NULL 0 // 默认客户端标记 ); if (pRet == NULL) { CString strErr; strErr.Format(_T("连接失败: %s"), mysql_error(pConn)); AfxMessageBox(strErr); mysql_close(pConn); return FALSE; } mysql_set_character_set(pConn, "utf8mb4"); MYSQL_RES* pResult = NULL; MYSQL_ROW row = NULL; if (mysql_query(pConn, "SELECT id, name FROM user_info") == 0) { pResult = mysql_store_result(pConn); int nCols = mysql_num_fields(pResult); while ((row = mysql_fetch_row(pResult)) != NULL) { // row[0]是id,row[1]是name // 注意:NULL字段在row里是NULL指针,不是空字符串 if (row[0] != NULL && row[1] != NULL) { int nId = atoi(row[0]); CString strName = CString(row[1]); } } } mysql_free_result(pResult); mysql_close(pConn);关键参数说明:mysql_options里设置连接超时3秒,可以避免服务器不可达时线程卡死。mysql_real_connect的端口参数必须正确,填错会报2003错误。查询的通用入口是mysql_query,但注意它只能执行一条语句,如果SQL里带分号写多条语句,需要mysql_real_query并在连接时开启CLIENT_MULTI_STATEMENTS标志。取结果集用mysql_store_result拉到本地,适合数据量小的场景;数据量大应该用mysql_use_result逐行读取,但使用期间不能发起新的查询,否则会丢结果。
4.3 多线程下C API的注意事项
MFC程序里多线程访问MySQL,最容易翻车的是每个线程共用一个MYSQL句柄。官方文档明确说了:MYSQL句柄默认不是线程安全的,一个句柄同一时刻只允许一个线程执行查询。合法做法是每个线程自己mysql_init创建一个独立句柄,需要注意用前初始化、用完释放。
如果一定要共享句柄,可以在初始化时设置mysql_thread_init配合库的线程安全编译版本(一般是libmysql.dll的标准版就已经支持线程安全,控制好互斥量即可)。实际操作中我很少共享句柄,因为连接本就很轻量,每个线程维持一个长连接的成本可控。另一个常见问题是:线程结束时忘记关闭句柄,导致连接泄漏。MySQL服务器端有一个max_connections上限,默认151个,连接数到了之后新连接会直接拒绝,报Too many connections——这种问题排查起来要命,因为代码看起来完全正常。
5. 避坑:MFC连接MySQL最常见的翻车现场
5.1 现象:连接字符串带SSL参数后报证书链错误
ODBC连接字符串里如果被加上了SSLMode=Required或者类似参数,MySQL 8.0的ODBC驱动默认会启用SSL加密,此时报[08001]证书链是由不受信任的颁发机构颁发的这种情况。原因是驱动默认不信任自签名证书,而本地MySQL默认用的就是自签名证书。
解决:如果不是公网环境,直接关掉SSL。在DSN配置里找到SSL Options,把SSLMode设为Disabled,连接字符串里对应的参数设为SSLMode=Disabled。注意:这个选项在ODBC 8.0驱动里的名称是SSLMode,在旧版5.3驱动里叫ssl-key/ca这类,要按安装的版本来。公网环境才建议保留SSL,并把自己的CA证书加到系统信任列表里。
5.2 现象:Release版连不上、Debug版能连上
同一套代码,Debug编译后连库正常,切到Release就报“无法连接”或者“找不到驱动”,这不是玄学,是ODBC驱动位数和程序位数不对齐。最常见的场景是:装了64位ODBC驱动,程序编译的却是x86架构。Debug下Maybe凑巧走了WOW64兼容层,Release的清单文件行为不同就暴露了。解决方法是打开配置管理器,确认当前是x64还是x86,然后按对应位数安装ODBC驱动。
一个更容易被忽略的坑:如果你在Visual Studio里改了平台目标,但ODBC DSN是在旧的位数下创建的,新平台下找不到DSN。注意这里要分清楚,用户DSN和系统DSN在不同位数注册表位置也不一样,64位系统上32位的DSN记录在HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\ODBC,这个注册表路径决定了它们是隔离的。
5.3 现象:CRecordset打开后中文变问号
中文乱码的原因多,但最常见的其实是有三层字符集不一致。比如:MySQL服务端是utf8mb4,DSN里Character Set没设置默认走了gbk,MFC工程多字节字符集。排查顺序:先查DSN的Character Set(最简单),再查MFC工程字符集,最后查服务端。我的固定配置是:服务端utf8mb4、DSN Character Set填utf8mb4、MFC工程属性里字符集选Unicode。如果数据库已有数据是gbk,新连接会按utf8mb4解读旧数据导致乱码,这种情况就固定在DSN层用gbk,保持和存量一致,不要混搭。
5.4 现象:程序启动后连接正常,跑一段时间后报Lost connection
长时间运行的程序间歇性报MySQL server has gone away,这个报错背后通常是wait_timeout把空闲连接回收了。MySQL默认的wait_timeout是8小时,但很多人部署时喜欢把它调成60秒或300秒以节省连接资源。MFC这边CDatabase不会自动重连,连接被服务端杀掉后第一次查询必然报错。
解决:要么把MySQL的wait_timeout调大,要么在MFC代码里做“断线重连”。我习惯在业务层封装一个bool PingDatabase(),每次操作前检查连接状态,如果为假或者查询失败就执行重连逻辑。C API下可以用mysql_ping函数,ODBC下没有直接等价物,对CDatabase常见做法是执行SELECT 1测活。
5.5 现象:数据库字段类型和GetFieldValue参数类型不匹配
CRecordset的GetFieldValue在取TINYINT、SMALLINT、INT、BIGINT、FLOAT、DOUBLE时的参数类型各有约定,比如BIGINT要用long long或__int64,FLOAT要用float,DOUBLE用double。如果类型对不上,MFC会在运行时抛DB_ROW_GET异常。排查办法很简单:先用rs.GetFieldValue(0, strValue)把所有字段当字符串取出来打印,看到实际值再决定类型。我遇到过的最典型情况是MySQL中COUNT(*)返回的是BIGINT,代码里却用int接了,数据超过21亿才会炸,但在小数据量时完全不报错——这种类型边界问题就靠“先打印后定类型”的笨办法解决。
6. 封装一层自己的MFC MySQL访问层:结构、验证与优化习惯
最后落地一步:把前面的经验收拢成一个精简的访问类,避免每个对话框里都裸写连库代码。类设计分三块:连接管理、查询执行、结果集封装。连接管理负责Open和Close、断线重连;查询执行封装ExecuteSQL和参数化写操作;结果集封装把CRecordset的遍历逻辑收敛到统一的GetString/GetInt接口里,这样上层代码不需要关心ODBC的字段类型细节。
class CMySqlHelper { public: bool Init(CString strDSN, CString strUser, CString strPwd) { m_strDSN = strDSN; m_strUser = strUser; m_strPwd = strPwd; return Connect(); } bool Connect() { if (m_db.IsOpen()) m_db.Close(); CString strConn; strConn.Format(_T("DSN=%s;UID=%s;PWD=%s;"), m_strDSN, m_strUser, m_strPwd); m_db.SetLoginTimeout(3); return m_db.OpenEx(strConn, CDatabase::noOdbcDialog); } bool IsAlive() { if (!m_db.IsOpen()) return false; try { CRecordset rs(&m_db); rs.Open(CRecordset::forwardOnly, _T("SELECT 1")); return true; } catch (CDBException* e) { e->Delete(); return false; } } bool Execute(const CString& strSQL) { if (!IsAlive()) Connect(); try { m_db.ExecuteSQL(strSQL); return true; } catch (CDBException* e) { e->Delete(); return false; } } private: CDatabase m_db; CString m_strDSN, m_strUser, m_strPwd; };这套结构在使用时有几个顺手的小技巧:每次操作前先IsAlive测活,避免Lost connection重灾区;所有写操作统一走Execute,方便日后加日志;析构函数里务必调用m_db.Close(),否则句柄泄漏会导致程序退出时数据库连接还悬着。如果你的程序是Dialog风格,建议把CMySqlHelper声明为主对话框的成员变量,程序退出时自动析构,不要在OnInitDialog里手动new——那样容易忘释放。
最后验证时,我习惯做三件事:连一次库看版本号;插入一万条数据看完成时间和资源占用;模拟断网看重连是否自动恢复。这三项通过,基本就可以放心交出去了。经验上最常翻车的不是代码本身,而是忘记检查测试机上的ODBC驱动和字符集配置——我就是被自己坑过好多次,才养成了每换一台机器先查驱动位数的习惯。希望帮到你。
本文还有配套的精品资源,点击获取