MFC连接MySQL实战:ODBC驱动、CDatabase与CRecordset避坑指南
简介面向Windows桌面开发者的微软基础类库MFC连接MySQL数据库的示例工程适合有一定C基础、需要在MFC程序中集成数据库的初学者与进阶开发者。资源围绕开放数据库连接ODBC方式展开完整呈现了从MySQL ODBC驱动安装、系统数据源DSN创建到CDatabase连接、CRecordset查询修改数据的编码流程并涉及异常捕获、事务提交与回滚以及资源释放等关键细节。工程名为DatabaseTest共38个文件压缩包仅3.54MB内含5个头文件、4个C源文件作为核心源码配合工程配置、调试生成文件如dsw、dsp、obj、pdb和界面资源文件图标、对话框等可直接用Visual Studio打开编译查看还有说明文档辅助理解。目前已有1664人学习下载。这份实例给出了一个可直接运行的MFCODBCMySQL小程序读者既可对照代码学习数据库类的标准用法也能将其中封装好的数据操作类与UI事件处理迁移到自己的项目中减少环境配置和编码排错时间。1. MFC 连接 MySQL 为什么先认 ODBC从选型到一次连通接手老 MFC 工程时最常遇到的需求就是把数据源从 Access 或 SQLite 迁到 MySQL。很多开发者在 C API、ADO、ODBC 之间反复纠结其实 MFC 原生封装的 CDatabase 和 CRecordset 就是基于 ODBC 的从 VC6 到 VS2022 的工程都能直接用不用额外引入 COM 初始化也不用对着 MYSQL 结构体一个个手工释放。这套方案的优点是改动集中在连接串和少量代码上老工程迁移成本最低调试时还能借助 ODBC 管理器看清底层状态。如果你是维护遗留 MFC 代码、或者想给桌面程序配一个稳定的 MySQL 后端ODBC 这条路线值得优先考虑。下面把驱动安装、DSN 配置、读写实战和常见坑完整走一遍。2. 环境准备与 DSN 配置驱动位数的第一个坑很多人连不上 MySQL 不是代码问题而是驱动没装对。这里面的关键点是位数匹配MFC 程序编译成 32 位驱动就必须是 32 位编译成 64 位驱动就必须是 64 位。系统是 64 位的不代表你的程序能加载 64 位驱动VS 里默认的 Win32 平台经常让你在 64 位系统上踩进 32 位的坑反之亦然。2.1 装对驱动Connector/ODBC 的版本与位数MySQL 官方提供的 ODBC 驱动分 5.x 和 8.x 两条线。5.3 的驱动对 MySQL 5.7 及以下版本很稳但服务端是 MySQL 8.0 且默认认证插件是 caching_sha2_password 时老驱动可能握手失败8.x 的 Unicode 驱动同时兼容 MySQL 5.7 和 8.0所以新项目我一般直接装“MySQL ODBC 8.0 Unicode Driver”。安装包里有 Unicode 和 ANSI 两个版本MFC 工程如果是 Unicode 字符集VS2010 以后默认就是选 Unicode Driver 即可。驱动装好之后可以用注册表命令确认当前机器上有哪些 ODBC 驱动。这里有一个容易忽略的地方32 位和 64 位驱动的注册表路径不一样查询时要分开看。REM 查看 64 位驱动 reg query HKLM\SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers REM 查看 32 位驱动64 位系统上 reg query HKLM\SOFTWARE\WOW6432Node\ODBC\ODBCINST.INI\ODBC Drivers还可以用 ODBC 数据源管理器直接看。64 位系统里有两个 odbcad32.exe%windir%\system32\odbcad32.exe 是 64 位管理器%windir%\SysWOW64\odbcad32.exe 是 32 位管理器。两个管理器看到的驱动和 DSN 列表完全独立你在 32 位管理器里配了数据源64 位程序是找不到的。这个现象我见过太多次了代码、驱动都装对了最后发现是 DSN 配置在了另一个管理器的视角里。排查思路是先确认程序编译平台再按平台装驱动最后用对应位数的管理器验证。如果程序是 x86就在 SysWOW64 的 ODBC 管理器里看驱动是否存在。这一步花两分钟能省掉后面一小时的排查时间。2.2 创建 DSN 还是写 DSN-less 连接串两种方式的取舍ODBC 连接有两种方式一种是先在系统里配置一个 DSN数据源名代码里用 DSN 去连接另一种是代码里直接写完整的驱动连接串不依赖系统配置。DSN 方式适合调试阶段因为可以在 ODBC 管理器里点“配置”按钮改服务器地址、端口、用户名不用重新编译程序。但部署到客户机器时要手工去每台机器上建 DSN稍微麻烦一点。DSN-less 连接串则把全部信息写在代码里客户机器只要装了驱动就能跑企业内批量部署更省事。我一般在开发阶段用 DSN发布版本换成 DSN-less两套方案都留着。DSN 可以由用户手工配置也可以在程序里调用 SQLConfigDataSource 自动创建。这个 API 适合做安装程序时的自动化配置避免让客户去控制面板里点。典型代码如下#include odbcinst.h #pragma comment(lib, odbcinst.lib) BOOL CreateMySqlDSN() { // szAttributes 以 \0 分隔各键值对结尾需要两个 \0 TCHAR szAttributes[] _T(DSNmy_mysql\0) _T(SERVER127.0.0.1\0) _T(PORT3306\0) _T(DATABASEtest\0) _T(UIDroot\0) _T(PWD123456\0) _T(CHARSETutf8\0) _T(\0); BOOL bRet SQLConfigDataSource( NULL, // 父窗口句柄NULL 表示不弹对话框 ODBC_ADD_SYS_DSN, // 添加系统 DSN所有用户可用 _T(MySQL ODBC 8.0 Unicode Driver), // 驱动名称必须和已安装驱动一致 szAttributes ); if (!bRet) { // 获取错误码常见原因驱动名写错、驱动未安装 DWORD dwErr SQLInstallerError(1, NULL, NULL, NULL, NULL, NULL); TRACE(_T(SQLConfigDataSource failed, error%lu\n), dwErr); } return bRet; }这段代码里ODBC_ADD_SYS_DSN 表示创建系统级 DSN对所有 Windows 用户可见不需要管理员权限的情况不多实际部署时建议用 ODBC_ADD_SYS_DSN 并确保程序以管理员身份运行。szAttributes 的格式比较特殊键值对之间用单个 \0 分隔整个字符串以两个 \0 结尾少一个 \0 就会导致最后一个参数读不到。驱动名必须和注册表里显示的完全一致比如 MySQL 8.0 驱动全名是“MySQL ODBC 8.0 Unicode Driver”写成“MySQL ODBC 8.0 Driver”就找不到。DSN-less 方式的连接串则集中在一行字符串里MFC 的 CDatabase 可以直接用CString strConn _T( Driver{MySQL ODBC 8.0 Unicode Driver}; SERVER127.0.0.1; PORT3306; DATABASEtest; UIDroot; PWD123456; CHARSETutf8; );注意 Driver 花括号里的驱动名要和系统安装的一致CHARSETutf8 这个参数要放在连接串里否则后面读中文容易出现乱码。端口默认 3306如果是自定义端口PORT 参数要显式写上。这个连接串也是我后面所有 CDatabase::OpenEx 调用里直接用的格式区别只是把 DSNmy_mysql 换成 DriverMySQL ODBC 8.0 Unicode Driver 而已。3. CDatabase 与 CRecordset 实战连接、查询和字段读出的标准姿势环境准备好之后代码层就是 MFC 的招牌组合CDatabase 负责建立连接CRecordset 负责执行查询和操作结果集。这两个类把 ODBC 的句柄、绑定、游标管理都封装掉了写起来比直接调 SQLAllocHandle 和 SQLFetch 清爽得多。3.1 CDatabase::OpenEx 连接连接串与选项参数怎么配CDatabase 的 OpenEx 是建立连接的主要入口它的第一个参数是连接串第二个参数是选项标志。最常用的标志是 noOdbcDialog表示禁止 ODBC 管理器弹出连接对话框否则连接失败时程序会弹出一个原生的 ODBC 登录框在后台服务或无人值守场景里会卡住进程。默认的 dwOptions 为 0表示允许弹出对话框所以生产代码一定要显式传 noOdbcDialog。BOOL ConnectToMySql(CDatabase db, const CString strConn) { try { // 第二个参数noOdbcDialog 禁止弹 ODBC 登录框 // openReadOnly 只读需要写入时不要加 BOOL bOK db.OpenEx(strConn, CDatabase::noOdbcDialog); if (!bOK) { // OpenEx 返回 FALSE 表示连接失败但通常这之前会抛异常 AfxMessageBox(_T(连接 MySQL 失败)); return FALSE; } return TRUE; } catch (CDBException* e) { // e-m_strError 是 ODBC 驱动返回的错误文本 // e-m_strStateNativeOrigin 形如 S1000 [MySQL][ODBC 8.0 ...] AfxMessageBox(e-m_strError); e-Delete(); return FALSE; } }调用示例CDatabase db; CString strConn _T( Driver{MySQL ODBC 8.0 Unicode Driver}; SERVER127.0.0.1;PORT3306;DATABASEtest; UIDroot;PWD123456;CHARSETutf8; ); if (ConnectToMySql(db, strConn)) { // 连接成功后续 CRUD 都基于 db }OpenEx 的返回值在大多数失败场景下其实不会安静地返回 FALSE而是直接抛 CDBException。所以 try-catch 是必须的。捕获后 CDBException 对象要用 Delete() 释放不要直接 delete因为 MFC 异常对象是从全局异常池里分配的。e-m_strError 里通常会带 MySQL 驱动自身的错误描述比如 “Access denied for user”可以直接弹给用户或写日志。连接串里的 CHARSETutf8 对应的是客户端字符集连接建立时驱动会执行 SET NAMES 一类的初始化这个参数直接影响后面读写中文的表现。还有一个细节CDatabase 对象在重复连接前要先 Close否则第二次 OpenEx 会报“连接已打开”。如果是程序启动时连接一次、运行期间一直复用这个不重要但做了重连逻辑的OpenEx 前必须判断 IsOpen 并 Close。3.2 CRecordset 查询遍历结果集与字段类型转换连接建立后查询有两条路线。执行 SELECT 用 CRecordset执行 UPDATE、DELETE 或 INSERT 用 CDatabase::ExecuteSQL。先看 SELECT 的典型写法CRecordset rs(db); try { // forwardOnly只允许向前移动游标内存占用最小 // snapshot结果集快照支持前后滚动适合小数据量 BOOL bOpen rs.Open( CRecordset::forwardOnly, _T(SELECT id, name, age FROM user WHERE age 18) ); if (!bOpen) { AfxMessageBox(_T(查询失败)); return; } while (!rs.IsEOF()) { CDBVariant varId, varName, varAge; rs.GetFieldValue((short)0, varId, SQL_C_SLONG); rs.GetFieldValue((short)1, varName, SQL_C_CHAR); rs.GetFieldValue((short)2, varAge, SQL_C_SLONG); long id varId.m_lVal; CString strName; if (varName.m_dwType DBVT_STRING varName.m_pstringA ! NULL) strName *varName.m_pstringA; long age varAge.m_lVal; TRACE(_T(id%ld, name%s, age%ld\n), id, strName, age); rs.MoveNext(); } rs.Close(); } catch (CDBException* e) { AfxMessageBox(e-m_strError); e-Delete(); }这个循环里GetFieldValue 用序号取字段时类型要匹配SQL_C_SLONG 取整型SQL_C_CHAR 取字符串。CDBVariant 是个判别联合体m_dwType 指示当前存放的类型DBVT_LONG 对应 longDBVT_STRING 对应 CString 指针。字符串取出后要判断指针非空再解引用这是新手最容易崩的地方。字段序号从 0 开始和 SELECT 列顺序一致如果 SQL 里写了具体的列名序号顺序就是列名的书写顺序。按序号取值比按列名快但可读性差。代码里最好在 SELECT 语句处加注释说明列顺序。另一种做法是用 DoFieldExchange 的 RFX 机制在 CRecordset 子类里声明成员变量重写 DoFieldExchange 绑定列。RFX 适合固定的表结构、编译期就知道字段的情况GetFieldValue 适合动态 SQL我一般做工具类和调试代码时用后者。非查询操作直接用 ExecuteSQL 就行不需要游标简单直接try { db.ExecuteSQL(_T(UPDATE user SET age age 1 WHERE id 1001)); db.ExecuteSQL(_T(DELETE FROM log WHERE ts DATE_SUB(NOW(), INTERVAL 7 DAY))); } catch (CDBException* e) { AfxMessageBox(e-m_strError); e-Delete(); }ExecuteSQL 返回类型是 void执行失败也是抛 CDBException所以判断成功与否全靠 try-catch。批量 INSERT 也走 ExecuteSQL但性能优化要在事务那块做文章后面第 5 章会专门讲。4. MFC 连接 MySQL 的避坑排查乱码、驱动缺失与句柄泄漏这一章把我在实际项目里碰到过的高频问题整理出来。每一条都按“现象 → 原因 → 解决”的顺序写照着排查比自己翻 MSDN 快得多。4.1 字符乱码连接串缺了 CHARSET 就全盘皆输现象SELECT 读出来的中文是“????”或者一串乱码向数据库写入中文后在 Navicat 里看是乱码但代码里读出来又是对的。原因连接串里没写 CHARSETutf8或者写的和数据库表的排序规则不一致。MySQL 服务端、客户端、连接三个环节的字符集各自独立ODBC 驱动默认连接字符集可能不是 utf8MFC 工程如果是 Unicode 字符集读回来的字节流还要从 UTF-8 转成宽字符。解决第一步是把连接串补上 CHARSETutf8这能解决大多数场景。第二步确认表的字符集是 utf8mb4 而不是 latin1如果是 latin1要把表转成 utf8mb4命令是ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第三步如果代码读取时仍然有乱码需要手动做一次 UTF-8 到宽字符的转换。MySQL 的 UTF-8 字节流在 MFC Unicode 工程里不能直接赋给 CString得用 MultiByteToWideChar// 假设从 CDBVariant 里取到的数据是 char* 的 UTF-8 字节 const char* utf8Str varName.m_pstringA-GetBuffer(0); // 第一步获取转换后宽字符的长度 int wLen MultiByteToWideChar( CP_UTF8, // 源编码是 UTF-8 0, // 默认转换选项 utf8Str, // 源字符串 -1, // 源字符串以 \0 结尾自动计算长度 NULL, // 先不填目标缓冲区 0 // 目标缓冲区大小为 0只获取长度 ); // 第二步按长度分配缓冲区并执行转换 CStringW wstr; if (wLen 0) { MultiByteToWideChar(CP_UTF8, 0, utf8Str, -1, wstr.GetBuffer(wLen), wLen); wstr.ReleaseBuffer(); }注意 GetBuffer 和 ReleaseBuffer 必须成对调用中间不能提前 return。CP_UTF8 是 Windows 里对 UTF-8 的代码页标识不要写成 CP_ACP。如果发现转换后中文正常说明连接串的 CHARSET 参数没生效或者驱动默认字符集不对回到第一步检查连接串里的书写格式。还有一个隐性坑ODBC 驱动版本不同默认连接字符集行为不同。5.3 驱动有时能正常读 UTF-88.0 驱动在连接串缺 CHARSET 时读中文就乱这个差异没有文档明确写属于只能靠经验覆盖的范畴。4.2 驱动加载失败与“找不到数据源”的排查清单现象OpenEx 抛出异常错误文本是 “Data source name not found and no default driver specified”或者 “Specified driver could not be loaded”。有时错误文本里直接带 [MySQL][ODBC] 字样但就是连不上。原因前一种是代码里用了 DSNxxx但系统 DSN 不存在或者 32/64 位不匹配导致在错误的 ODBC 管理器视角里找不到后一种是连接串里 Driver{...} 名称和已安装的驱动名不一致或者根本没装对应位数的驱动。解决按下面的顺序排查每一条都能快速定位。第一确认程序编译平台。VS 菜单栏看解决方案平台的 x86 还是 x64。第二用对应位数的 ODBC 管理器确认驱动存在。32 位程序就运行 %windir%\SysWOW64\odbcad32.exe在里面找到“连接器/ODBC”页签看“MySQL ODBC 8.0 Unicode Driver”是否在列表里。第三检查连接串里的驱动名。驱动名是注册表里的完整名称少一个空格、多一个字符都不行用注册表命令精确核对reg query HKLM\SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers /v MySQL ODBC 8.0 Unicode Driver如果命令返回找不到说明驱动没装或者位次不对。第四确认 MySQL 服务端口可通。在命令行用 telnet 或 PowerShell 的 Test-NetConnection 验证 3306 端口排除防火墙档连接的场景。ODBC 驱动报错和网络不通报错不同网络不通时错误文本是 “Cant connect to MySQL server on 127.0.0.1 (10061)”端口通了再回头查驱动。最常见的情况其实是驱动装了两个位数的版本但程序是 x86系统 ODBC 管理器默认打开的是 64 位视角看到的驱动列表和程序能加载到的驱动列表不一样。这时候用 SysWOW64 的 odbcad32.exe 看一眼就真相大白。4.3 句柄泄漏与析构崩溃现象程序运行一段时间后内存持续增长或者关闭时在 CDatabase / CRecordset 析构处崩溃报 “Debug assertion failed”断言位置在 dbcore.cpp 附近。原因CRecordset 持有 ODBC 语句句柄CDatabase 持有连接句柄两者的析构顺序有依赖。CRecordset 必须先于 CDatabase 析构否则 CDatabase 析构时发现还有未关闭的结果集在工作直接断言。另一种泄漏场景是 new 出来的 CDatabase 没 delete这个比较低级但确实常见。还有一种隐蔽场景OpenEx 抛异常后CDatabase 内部状态半开未做 Close 就析构同样会触发问题。解决坚持两个习惯。第一所有连接和结果集都用栈对象不要 new。栈对象在出作用域时按逆序自动析构天然满足“结果集先关、连接后关”的顺序。第二OpenEx 或 Open 失败后显式调用 Close清理半开状态。void QueryUser() { CDatabase db; // 栈对象函数结束自动 Close CRecordset rs(db); // 栈对象先于 db 析构 try { if (!db.OpenEx(strConn, CDatabase::noOdbcDialog)) return; rs.Open(CRecordset::forwardOnly, _T(SELECT id FROM user)); while (!rs.IsEOF()) { // ... rs.MoveNext(); } } catch (CDBException* e) { // 异常时 rs 可能处于打开状态 if (rs.IsOpen()) rs.Close(); if (db.IsOpen()) db.Close(); e-Delete(); } }如果非要把连接封装成类成员注意成员声明顺序。类成员的析构顺序和声明顺序相反所以要先声明 CDatabase再声明 CRecordset这样析构时结果集先销毁连接后销毁。如果反过来声明结果集析构时连接已经没了必然崩溃。还有一类情况模块卸载时全局 CDatabase 对象析构但某些驱动 DLL 已经卸载。这个属于极端场景通常出现在 MFC 扩展 DLL 里。解决办法是把全局连接改成函数内静态变量确保在首次使用时初始化、在进程退出时才析构避免模块间生命周期交叉。5. 参数化查询与事务处理把数据安全压实前面几章解决了连接和基础查询但实际项目中还有一个绕不开的问题SQL 注入和写入性能。这两件事放在一起处理是因为它们都发生在 SQL 执行层而且 MFC 的 ODBC 封装给了一套相对完整的解决方案。5.1 参数化查询别手工拼 SQL让 ODBC 处理参数绑定写 MFC 程序时最容易出现的坏习惯是字符串拼接 SQL。比如用户输入一个用户名代码写成CString sql; sql.Format(_T(SELECT * FROM user WHERE name %s), userInput); db.ExecuteSQL(sql);userInput 里如果包含单引号比如admin OR 11SQL 语义就变了。这不是危言耸听MFC 程序用 CString 拼 SQL 太顺手几乎每个老工程都能扫出一大片。正确做法是让 ODBC 驱动做参数绑定CRecordset 的 RFX 机制支持声明参数成员。class CUserRs : public CRecordset { public: CUserRs(CDatabase* pDB) : CRecordset(pDB) { m_nFields 2; // 两个输出列id, name m_nParams 1; // 一个参数name m_id 0; m_strName.Empty(); m_strNameParam.Empty(); } virtual CString GetDefaultSQL() { return _T(user); // 表名 } virtual void DoFieldExchange(CFieldExchange* pFX) { // 输出列绑定 pFX-SetFieldType(CFieldExchange::outputColumn); RFX_Long(pFX, _T(id), m_id); RFX_Text(pFX, _T(name), m_strName); // 参数绑定参数在 SQL 中用 ? 占位 pFX-SetFieldType(CFieldExchange::param); RFX_Text(pFX, _T(name_param), m_strNameParam); } public: long m_id; CString m_strName; CString m_strNameParam; };使用时代码变成CUserRs rs(db); rs.m_strFilter _T(name ?); // ? 是参数占位符 rs.m_strNameParam _T(admin); // 给参数赋值 rs.Open(CRecordset::snapshot, NULL, CRecordset::readOnly); while (!rs.IsEOF()) { // rs.m_id, rs.m_strName 已经是绑定好的字段值 TRACE(_T(id%ld, name%s\n), rs.m_id, rs.m_strName); rs.MoveNext(); }这里的关键是 m_nParams 必须和 DoFieldExchange 里 param 类型的 RFX_Text 数量一致。CRecordset 在执行 Open 时会把 m_strFilter 里的 ? 替换为参数成员的实际值再交给 ODBC 做绑定整个过程不经过字符串拼接注入路径被堵死。m_strNameParam 赋值长度不要超过表字段定义长度否则 MySQL 报 “Data too long for column”这个错误信息会通过 CDBException 抛出来。如果只是做单条 UPDATE 或 DELETE参数化查询用起来要绕一点毕竟 ExecuteSQL 不支持参数占位符。这时可以改用 CDatabase::BeginTrans 配合 ExecuteSQL 拼固定值 SQL只要值做了严格转义比如把单引号替换成两个单引号安全性也可控。但从工程规范看能走参数化的地方尽量走参数化。5.2 事务与批量提交写入性能和安全性的取舍默认情况下 MySQL 的 autocommit 是开启的每条 ExecuteSQL 都独立提交。批量写入几千条时需要每条都写盘一次性能损耗很大中途某条失败时已经成功提交的记录也无法回滚。这时需要手动控制事务。void BatchInsert(CDatabase db) { try { // 开启事务ODBC 层会关闭 autocommit if (!db.BeginTrans()) { AfxMessageBox(_T(BeginTrans 失败)); return; } // 分批提交每 500 条提交一次避免事务日志过大 for (int i 0; i 5000; i) { CString sql; sql.Format( _T(INSERT INTO t_log (msg, ts) VALUES (item_%d, NOW())), i ); db.ExecuteSQL(sql); if ((i 1) % 500 0) { db.CommitTrans(); // 提交当前一批 db.BeginTrans(); // 开启下一批事务 } } db.CommitTrans(); // 提交最后一批 } catch (CDBException* e) { // 失败时回滚当前未提交的事务块 db.Rollback(); e-ReportError(); e-Delete(); } }这段代码里有一个容易被忽略的点catch 里的 Rollback 回滚的是最后一次 BeginTrans 之后未提交的部分之前已经 CommitTrans 的批次无法回滚。所以“分批提交”本质上牺牲了全量原子性换来了更小的回滚代价和更低的锁占用。如果业务要求要么全部成功要么全部失败就不要在循环里 CommitTrans等循环结束后统一提交一次。全量提交在数据量大时会导致事务日志膨胀和锁竞争适合千条以下的场景。事务还有一个硬性前提MySQL 表引擎必须是 InnoDB。MyISAM 不支持事务BeginTrans 后执行 ExecuteSQL 不会报错但 CommitTrans 和 Rollback 都不会真正生效数据照常落盘。建表时务必确认 ENGINEInnoDB这个可以通过 SQL 检查SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA test;批量写入的性能还有一个变量MySQL ODBC 驱动支持在连接串里加 OPTION 参数比如 OPTION67108864 可以启用多语句执行把多条 INSERT 合并到一次往返。但多语句执行在 MFC 的 ExecuteSQL 上行为不稳定出错时报错位置也不好定位我不建议在老工程里用它。更稳妥的提升手段是减少 CommitTrans 次数从每条一提交改成几百条一提交性能提升已经非常明显。6. 验证连接与连接复用两个实用技巧收尾6.1 先跑一条诊断 SQL 验证版本和字符集连接配置完成后不要急着写业务代码先跑一条诊断 SQL 确认服务端版本、连接字符集和表引擎。这段 SQL 可以直接用 CDatabase::ExecuteSQL 配合临时表来执行或者用 CRecordset 读取SELECT VERSION() AS ver, character_set_database AS db_charset, character_set_connection AS conn_charset, sql_mode AS sql_mode;用 CRecordset 打开这条查询把 ver 和 conn_charset 打出来。如果 conn_charset 不是 utf8mb4 或 utf8说明连接串的 CHARSET 参数没有生效。注意驱动客户端版本和服务端版本可能不一致但连接字符集必须对齐否则中文读写迟早出问题。这条验证我每次配完新环境都跑一遍算是刚需动作。6.2 连接复用别把 CDatabase 当临时变量反复创建MFC 程序里每执行一次查询就创建一个 CDatabase 并 OpenEx然后 Close会让 MySQL 服务端频繁地建立和销毁连接短暂业务场景看不出问题但循环查询的场景下性能差异明显。另外 MySQL 服务端有 max_connections 限制连接泄漏多了会报 “Too many connections”。我一般把连接做成进程级单例整个程序生命周期只建一次。实现方式很简单用函数内静态变量确保延迟初始化CDatabase GetDatabase() { static CDatabase db; // 首次调用时构造进程退出时析构 return db; }调用方统一走 GetDatabase()不再自己管理连接的生命周期。如果程序是多线程的注意 CDatabase 不是线程安全的所有数据库操作要通过同一个线程或外部加锁串行化否则 ODBC 句柄并发访问会随机崩溃。这一点我踩过坑当时开四个工作线程同时读表偶发性崩溃查了很久最后发现是 CDatabase 并发访问问题。从那以后我每次连接相关的代码评审都会先看连接对象的作用域和调用线程是不是单一且清晰的。连接串、驱动位数、字符集三个要素确认无误后跑一遍诊断 SQL这套流程走下去基本不会出大问题希望帮到你。本文还有配套的精品资源点击获取