简介这是一套面向MFC桌面应用开发者的MySQL数据库访问示例工程核心演示如何借助ODBC标准数据库接口在C程序中完成连接建立、SQL查询、结果集遍历与数据更新等常用操作。压缩包内共含117个文件压缩后约181KB其中49个.c与46个.h构成完整源码主体另有dsp/dsw工程文件、mak/makefile构建脚本、rc资源描述及def/plg等辅助配置便于在Visual C环境中直接编译调试或迁移至其他项目几个.001分卷文件则对应ODBC相关库内容按序解压即可使用。已有145人浏览学习该资源。示例代码覆盖了MFC ODBC编程的典型环节包括初始化ODBC模块、通过CDaoDatabase连接数据源、利用CDaoRecordset执行SQL及移动记录、编辑更新字段等同时展现出MySQL Connector/ODBC驱动配置与DSN设置的完整思路可帮助开发者快速掌握MFC与MySQL交互的框架与API调用方式也可作为开发小型数据库管理系统的实用参考模板。1. MFC 连 MySQL 的工程现场一个 ODBC 封装包为什么值得反复琢磨看到MySQL.rar这种命名的压缩包老 Windows 开发心里基本有数里面多半是散装的 MFC MySQL 工程零件有人用 Visual Studio 写对话框程序通过 ODBC 把CDatabase和CRecordset接到 MySQL 上代码流传了几轮注释还带着拼音。这套东西解决的是桌面端最朴素的需求——老 MFC 工具要直连 MySQL 5.7 或 8.0 做增删改查不引入重型 ORM不重写成 Qt 或 Web。适合谁维护老工程、要给内部工具加数据库面板、又不想把时间耗在第三方库配置上的 Win32 开发者。读完之后你能从选型、环境到代码把整条链路一次理清也知道问题出现时先查哪里。2. 选型MFC 连 MySQL 的四条路ODBC 凭什么最省心2.1 四条路线对比C API、ODBC、ADO、Connector/C 的账MFC 程序连 MySQL常见做法是四条路MySQL 官方 C API、ODBC、ADO、Connector/C。看到类似c_MySql字样的压缩包时很多人第一反应是找 libmysql 的动态库其实后面三条路线都不需要在业务代码里堆第三方头文件。把四条路摆在一起算账选型就清楚了路线需要额外安装的东西典型调用方式维护成本适合场景MySQL C APIlibmysql.dll、头文件mysql_init / mysql_real_query高结构题和句柄全自己管嵌入式、无 UI 的 C 程序ODBCMySQL ODBC DriverCDatabase / CRecordset低MFC 已封装大半现有 MFC 工程ADO需 ADO 运行时_ConnectionPtr / _RecordsetPtr中COM 指针繁琐VB 脚本迁移过来的老代码Connector/C官方库和大量依赖类似 JDBC 的 API中高版本兼容麻烦新 C 项目但不走 MFC在 MFC 工程里ODBC 是回报率最高的一条。理由有三第一MFC 从 VC6 时代就把CDatabase、CRecordset焊死在框架里接手老工程的人不需要学新 APIMSDN 上的例子直接能抄。第二ODBC 连接串标准统一今天连 MySQL明天要换 SQL Server只改驱动名和连接参数业务代码几乎不动。第三驱动由 MySQL 官方维护Windows 装完即用不用把一堆运行时 dll 塞进安装包。Connector/C 看起来更现代但它在 Visual Studio 工程里的配置项太多Debug/Release、x86/x64 各要配一遍还经常因为运行时版本不一致在客户机器上当场报错。桌面工具这种量级的查询ODBC 的封装开销可以忽略。选型的血泪经验就一句老工程别引入新依赖除非有预算把整条链路重写一遍。2.2 环境搭建odbc 连接字符串与 MySQL ODBC 驱动的安装闭环选完型先把环境搞对。常见做法是去 MySQL 官网下载 ODBC Driver版本选 8.0 系列它同时兼容 MySQL 8.0 和 5.7 的服务端。注意两点安装包分 32 位和 64 位你的 MFC 工程编译成什么平台就装对应位数驱动安装向导会检查 Microsoft Visual C 2015 Redistributable14.0 系列运行库干净机器上驱动装到一半失败八成是这个运行库没装。驱动装好后打开 ODBC 数据源管理器验证。Windows 搜索「ODBC 数据源」会出来两个入口32 位是C:\Windows\SysWOW64\odbcad32.exe64 位是C:\Windows\System32\odbcad32.exe。这地方像个黑匣子最常见的错觉是「装好了」——打开「驱动程序」页确认能看到MySQL ODBC 8.0 Unicode Driver才算真的装好。DSN 有三种用法用户 DSN存在当前 Windows 账号的注册表里只适合开发机自测。系统 DSN所有账号可见适合部署到服务器。无 DSN不配数据源直接在代码里写 odbc 连接字符串。最推荐部署机上少一步配置换机器不用重新配注册表。无 DSN 的连接串长这样这是全篇所有连接的基础CString strConn; strConn.Format( _T(Driver{MySQL ODBC 8.0 Unicode Driver};) _T(Server%s;Port3306;Database%s;) _T(User%s;Password%s;Option3;), strServer, strDb, strUser, strPwd);连接串里每个分号字段都要记清楚Driver必须和驱动名完全一致写错一个字母就报Driver not foundServer能用127.0.0.1就别写localhost某些环境里 localhost 被解析成 IPv6 的::1反而连不上Port默认 3306服务端改过端口这里必须跟着改Option3是老例子里的常见杂项开关照抄不会出错但别指望它解决权限问题。MySQL 服务端安装时如果勾选了 TCP/IP 监听记得确认防火墙放行 3306否则后面所有代码都死在超时上。注意32 位和 64 位的 MySQL ODBC 驱动可以共存但 MFC 工程的编译平台必须和驱动位数一致这条在部署阶段会反复回来找你。3. 从 CDatabase 到 CRecordset跑通第一条查询的最小可运行代码3.1 工程配置三件事afxdb.h、odbc32.lib 与字符集驱动装好只是前提MFC 工程这边还要开三条路。第一包含 MFC 的 ODBC 头文件第二让链接器带上 ODBC 导入库第三定死字符集。新建工程的pch.h老工程是stdafx.h里加两行// pch.h 顶部追加 #include afxdb.h // MFC 的 ODBC 类CDatabase、CRecordset #pragma comment(lib, odbc32.lib) // 链接 ODBC 导入库#pragma comment是给编译器看的链接指令等价于在「项目属性 → 链接器 → 输入 → 附加依赖项」里手动填odbc32.lib。两者选一个就行手写 pragma 的好处是换机器重配工程时不会漏。字符集在「项目属性 → 常规 → 字符集」里看。新工程默认是「使用 Unicode 字符集」对应的连接串 Driver 要用MySQL ODBC 8.0 Unicode Driver如果工程是历史遗留的多字节Multi-Byte工程可以用配套的MySQL ODBC ANSI Driver。MFC 的CString在 Unicode 工程里是CStringW在多字节工程里是CStringA连接串里的_T()宏会自动适配别自己手写L...前缀否则切字符集时整个工程都是编译错。3.2 封装一个 CMySqlConnection连接串、错误捕获与释放直接在一个按钮里 new 一个CDatabase也能跑但一个正经桌面工具通常有多个窗口和多个查询点我一般会先封装一个最小的连接类把连接串拼装、异常捕获、资源释放收敛到一处// MySqlConnection.h #pragma once #include afxdb.h class CMySqlConnection { public: CMySqlConnection(); virtual ~CMySqlConnection(); bool Connect(const CString strServer, const CString strUser, const CString strPwd, const CString strDb, UINT nPort 3306); void Close(); CDatabase* GetDatabase() const { return m_pDb; } CString GetLastError() const { return m_strError; } private: CDatabase* m_pDb; CString m_strError; };// MySqlConnection.cpp #include MySqlConnection.h CMySqlConnection::CMySqlConnection() : m_pDb(nullptr) { } CMySqlConnection::~CMySqlConnection() { Close(); } bool CMySqlConnection::Connect(const CString strServer, const CString strUser, const CString strPwd, const CString strDb, UINT nPort) { if (m_pDb ! nullptr) return true; m_pDb new CDatabase; CString strConn; strConn.Format( _T(Driver{MySQL ODBC 8.0 Unicode Driver};) _T(Server%s;Port%u;Database%s;User%s;Password%s;Option3;), strServer, nPort, strDb, strUser, strPwd); try { // noOdbcDialog连接失败时不弹 ODBC 配置框避免客户机弹出莫名对话框 m_pDb-OpenEx(strConn, CDatabase::noOdbcDialog); } catch (CDBException* e) { m_strError e-m_strError; e-Delete(); delete m_pDb; m_pDb nullptr; return false; } return true; } void CMySqlConnection::Close() { if (m_pDb ! nullptr) { if (m_pDb-IsOpen()) m_pDb-Close(); delete m_pDb; m_pDb nullptr; } }这个类的关键点在OpenEx的第二个参数。noOdbcDialog是必带的——否则连接失败时 ODBC 会弹一个数据源配置对话框客户机器上没人知道怎么点。CDatabase*用裸指针是因为CDatabase没有拷贝语义析构函数里做Close()保证无论哪条路径退出都不漏。GetLastError把CDBException::m_strError留下来调试时比只看弹窗有用得多。3.3 第一条查询CRecordset 的 Open 与 GetFieldValue 列号连接就绪跑第一条查询。CRecordset可以直接用临时对象不需要每次写派生类——只读列表的简单场景这样最省事bool QueryNames(CDatabase db, const CString strSql, std::vectorCString rows) { CRecordset rs(db); try { // forwardOnly只能向前遍历只读列表时性能最好 rs.Open(CRecordset::forwardOnly, strSql); while (!rs.IsEOF()) { CString strValue; // 列号从 1 开始这里取第 1 列 rs.GetFieldValue((short)1, strValue); rows.push_back(strValue); rs.MoveNext(); } rs.Close(); return true; } catch (CDBException* e) { TRACE(_T(QueryNames failed: %s, SQL%s\n), e-m_strError, strSql); e-Delete(); if (rs.IsOpen()) rs.Close(); return false; } }CRecordset构造时接收CDatabase*Open的第一个参数指定游标类型。列表填充场景用forwardOnly就够了它不缓存行集内存占用最小只有需要来回滚动或原地编辑时才考虑snapshot和dynaset。GetFieldValue的列号从 1 开始传 0 会抛「参数索引越界」——这个坑几乎每个 MFC 新手都踩过后面避坑章还会提到。返回值类型用CString重载驱动会自动把数字和时间类型转成文本省去类型分支判断。4. 写实 CRUD参数化查询、事务回滚与 NULL 字段处理4.1 参数化查询m_strFilter 与 RFX 参数绑定的数量规则CRecordset直接拼 SQL 做查询虽然能跑但用户输入一旦带上单引号SQL 就被截断甚至被注入。正确做法是用 MFC 的参数化机制从CRecordset派生一个类在DoFieldExchange里用 RFX 宏声明参数class CUserSet : public CRecordset { public: CUserSet(CDatabase* pDb); virtual CString GetDefaultSQL(); virtual void DoFieldExchange(CFieldExchange* pFX); long m_id; CString m_name; CString m_strNameParam; // 绑定 WHERE 条件里的 ? }; CUserSet::CUserSet(CDatabase* pDb) : CRecordset(pDb) { m_id 0; m_name.Empty(); m_strNameParam.Empty(); m_nFields 2; // SELECT 结果有两列 m_nParams 1; // 有一个查询参数 } CString CUserSet::GetDefaultSQL() { return _T(SELECT id, name FROM t_user); } void CUserSet::DoFieldExchange(CFieldExchange* pFX) { RFX_Long(pFX, _T(id), m_id); RFX_Text(pFX, _T(name), m_name); // 参数不是结果列但 RFX 类型要和对应列类型一致 RFX_Text(pFX, _T(nameParam), m_strNameParam); }使用时设置m_strFilter里面的?会按声明顺序取参数值CUserSet rs(db); rs.m_strNameParam _T(张%); // 参数值和列类型一致都是文本 rs.m_strFilter _T(name LIKE ?); // ? 的个数必须和 m_nParams 相等 rs.Open(); while (!rs.IsEOF()) { // 这里读 rs.m_id、rs.m_name rs.MoveNext(); } rs.Close();m_nParams是这套机制里最容易被忽略的数字。SQL 里写了两个?m_nParams却只填 1打开记录集时驱动直接抛「参数数量不足」。参数类型也讲究name是varchar就用RFX_Textid是整型就用RFX_Long类型不匹配在绑定阶段不报错取值阶段才出奇怪结果排查成本极高。至于 INSERT 和 UPDATE常见做法是CDatabase::ExecuteSQL拼字符串前提是必须做单引号转义CString SafeSql(const CString strIn) { CString strOut strIn; strOut.Replace(_T(), _T()); // 单引号翻倍防止 SQL 被截断 return strOut; } CString strSql; strSql.Format(_T(INSERT INTO t_user(name, remark) VALUES(%s, %s)), (LPCTSTR)SafeSql(strName), (LPCTSTR)SafeSql(strRemark)); db.ExecuteSQL(strSql);写操作没有记录集参数绑定可用转义就是最后一道防线。内部工具不用武装到牙齿但这条习惯必须养成——哪天工具暴露到公网这条习惯能挡掉大部分脚本攻击。4.2 事务边界BeginTrans 到 CommitTrans 的完整节奏涉及账户、订单、库存这类数据两条 UPDATE 必须在一个事务里。MFC 的CDatabase自带事务方法完整节奏如下if (!db.CanTransact()) { // 驱动或表不支持事务再往下做就是自欺欺人 AfxMessageBox(_T(当前连接不支持事务请检查表引擎)); return false; } try { db.BeginTrans(); db.ExecuteSQL(_T(UPDATE t_account SET balance balance - 100 WHERE id 1)); db.ExecuteSQL(_T(UPDATE t_account SET balance balance 100 WHERE id 2)); // 业务校验全部通过再提交 db.CommitTrans(); } catch (CDBException* e) { TRACE(_T(事务失败: %s\n), e-m_strError); e-Delete(); db.Rollback(); // 出错回滚两条 UPDATE 都不生效 return false; }三个细节要注意。第一BeginTrans必须是该连接上第一条执行的语句不能前面跑了一堆查询再开事务。第二事务只对当前CDatabase连接生效两条 UPDATE 必须走同一个连接跨连接无法保证原子性。第三也是最大的坑MySQL 只有 InnoDB 引擎支持事务MyISAM 表会把BeginTrans静默吞掉Rollback也不报错但数据照样写进去。老库的表经常是 MyISAM用之前先确认引擎具体怎么查放避坑章讲。4.3 NULL 字段用 IFNULL 在前端 SQL 层兜底ODBC 对 NULL 的返回比较含蓄GetFieldValue取到 NULL 时不同类型的行为不一致CString重载拿到的是空串数字类型拿到的是 0。问题在于空串和 NULL 语义不同「没有备注」和「备注为空」在业务上可能一个允许一个不允许。与其在 C 端反复判断不如在 SQL 层直接兜底rs.Open(CRecordset::forwardOnly, _T(SELECT id, IFNULL(remark, ) AS remark FROM t_user));IFNULL(remark, )把 NULL 统一转成空串C 端少一个分支。但注意IFNULL的第二个参数类型决定返回类型金额字段用IFNULL(amount, 0)会把 NULL 和 0 混为一谈如果业务区分「未设置」和「金额为 0」就别用 0 兜底。另一种做法是取CDBVariant先看m_dwType再取值代码冗长但语义精确适合字段级要求严格的场景。我的习惯是查询路径能用 SQL 函数解决的问题绝不在 C 端加状态判断。5. 避坑手册MFC ODBC MySQL 的 5 个翻车现场5.1 中文乱码Unicode 工程配上了 ANSI 驱动现象查询出来的中文在 MessageBox 和 ListCtrl 里变成???或类似乱码英文和数字正常。原因工程是 Unicode 字符集连接串 Driver 却写成MySQL ODBC 5.3 ANSI Driver或反过来客户端与服务端字符集不一致。另一个常见源头是源文件本身没保存成带签名的 UTF-8字符串字面量在编译期就错了。解决连接串统一用MySQL ODBC 8.0 Unicode Driver并在连接串追加CharacterSetutf8mb4;。代码里的中文字符串一律用_T()包裹源文件用 Visual Studio 另存为「带有签名的 UTF-8」编码。改完这两处再执行一遍查询乱码通常当场消失。5.2 odbc missing32 位工程遇上 64 位驱动的错位现象驱动明明装了程序启动连接时报odbc missing或报「数据源名称未找到」。更诡异的是同事机器上同样代码能跑你的机器就是不行。原因MFC 工程编译成 x86系统里只装了 x64 的 MySQL ODBC 驱动或者 DSN 配在 64 位的 odbcad32 里程序以 32 位进程去读注册表当然找不到。这类问题被称作玄学本质是位数错位。解决先看工程平台是 Win32 还是 x64再去对应入口的 ODBC 数据源管理器「驱动程序」页确认驱动存在如果两边对不上装对应位数的驱动或改用无 DSN 连接串彻底绕开注册表。验证方法在连接串的Driver写错时错误信息里会带「找不到指定的驱动程序」看到这句先查位数别怀疑代码。5.3 MySQL 8.0 连不上caching_sha2_password 认证插件旧驱动不认现象连接串、端口、账号密码全对OpenEx抛Authentication plugin caching_sha2_password cannot be loaded。原因MySQL 8.0 默认认证插件是caching_sha2_password旧版 MySQL ODBC 驱动不认识新插件。5.x 的老驱动配 8.0 服务端十个有九个挂在这里。解决优先升级驱动到 8.0 系列的较新 Unicode 版本新驱动官方支持 MySQL 8.0 的认证插件。如果客户机不允许升级驱动临时方案是把账号改回旧插件ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;这条 SQL 有后悔药可吃改回caching_sha2_password也一样一条命令。但它是兼容性让步不是治本方案生产环境建议走升级驱动的路同时确认机器装了 VC 2015 运行库否则新驱动装不上又是下一个 odbc missing。5.4 事务不回滚MyISAM 表把 BeginTrans 静默吞掉现象事务代码完全照着文档写BeginTrans、CommitTrans、Rollback都调用成功但模拟崩溃时数据仍然写进去了两条 UPDATE 只成功一条。原因MySQL 5.5 之前的默认引擎是 MyISAM不支持事务。ODBC 层面对BeginTrans的调用被驱动静默接受MySQL 服务端执行 UPDATE 时根本没有事务上下文Rollback 自然什么都不做。解决先查引擎SHOW TABLE STATUS WHERE Name t_account;看到Engine列是MyISAM就执行ALTER TABLE t_account ENGINEInnoDB;新库建表时显式指定ENGINEInnoDB别依赖默认配置。代码里保留CanTransact()检查它返回 FALSE 时就拒绝继续执行宁可报错也不让用户以为有事务保护。5.5 参数绑定翻车m_nParams 与 SQL 里的 ? 数对不上现象打开派生记录集时抛Too few parameters. Expected 1.或者莫名其妙的Invalid parameter number。SQL 单独拷到 MySQL 客户端里能跑代码里就是报错。原因m_strFilter里写了两个?m_nParams却只填了 1或者DoFieldExchange里参数化 RFX 的声明顺序和 SQL 中?的出现顺序不一致。MFC 按声明顺序取参数顺序错位时数据绑到错误的列上。解决数清楚 SQL 里?的个数把它填进构造函数里的m_nParams然后在DoFieldExchange里按从左到右的顺序声明参数化 RFX 宏。调试时在DoFieldExchange入口下断点看pFX-m_nOperation是否按预期次数进入参数绑定分支。这个坑最气人的地方是编译期完全正常只在运行时炸所以参数个数是每次改 SQL 后第一件要核对的事。6. 进阶把查询扔进工作线程状态栏不再假死ODBC 查询走网络MySQL 服务端稍慢一点界面线程就卡住鼠标转圈十几秒。桌面软件的体面做法是把查询放到工作线程。这里有一条铁律CDatabase和CRecordset都不是线程安全的一个连接对象不能在两个线程里同时用线程可以有自己的连接但绝不允许把主线程的CDatabase*传进AfxBeginThread的参数里。常见做法是在工作线程内部 new 一个CMySqlConnection独立连接、独立查询。线程结束不能直接操作 UI 控件MFC 的标准姿势是PostMessage把结果指针丢回界面线程UINT QueryWorker(LPVOID pParam) { CMySqlConnection conn; if (!conn.Connect(g_strServer, g_strUser, g_strPwd, g_strDb)) { // 失败也发消息让界面线程弹错 ::PostMessage(g_hMainWnd, WM_QUERY_DONE, 0, 0); return 1; } std::vectorCString rows; QueryNames(conn.GetDatabase(), g_strSql, rows); CString* pResult new CString; // 堆指针跨线程传递 for (size_t i 0; i rows.size(); i) *pResult rows[i] _T(\r\n); ::PostMessage(g_hMainWnd, WM_QUERY_DONE, 1, (LPARAM)pResult); return 0; }界面线程用ON_MESSAGE(WM_QUERY_DONE, CMainDlg::OnQueryDone)映射处理函数在函数里把pResult转出来填充CListCtrl最后delete pResult顺手用m_wndStatusBar.SetPaneText(0, _T(查询完成))刷新状态栏。别用SendMessage它会等界面线程处理完才返回等于又阻塞了PostMessage异步投递工作线程发完就退出。跨线程传指针务必在接收端 delete这是老 MFC 工程内存泄漏的最大来源我曾经在一个工具里漏了三年。我自己的习惯是把「线程 PostMessage 回传」封装成一个RunAsyncQuery(SQL, 窗口句柄)的包装函数业务按钮只传 SQL 和接收消息的 HWND再顺手把m_strError连同 SQL 写进本地日志文件。远程客户机器上的 ODBC 报错弹窗只能截个图日志却能定位到底是连接、认证还是 SQL 哪一环出了问题。封装越早做完后面每个查询按钮就只剩一行调用希望帮到你。本文还有配套的精品资源点击获取