ODBC连接Access数据库:VC6.0老项目维护实战指南
简介这份ODBC与VC6.0结合的数据库访问学习资源适合初学C数据库编程的开发者帮助理解如何通过ODBC接口连接并操作Access数据库。压缩包内共278个文件以h头文件、cpp源文件、obj目标文件及sbr浏览器信息文件为主并附带mdb数据库、可执行exe及项目工程文件整体约4.81MB结构完整便于对照源码学习MFC封装下的CDatabase、CRecordset等类库用法。资源已有226人学习其中包含一个学生信息管理系统的ODBC示例可从建库、配置数据源到增删改查完整演示经典CRUD操作流程。通过分析代码与运行调试能够快速上手VC6.0环境下访问Access数据库的常用开发方式为后续接入其他数据库打下基础。1. 一个 ODBC/access/vc6.0 压缩包牵动多少老项目的续命工程硬盘里翻出一个“ODBC.rar”解压后是一整个 VC6.0 的工程里面要么是 ODBC 访问 Access 数据库的示例源码要么就是某套老系统的核心模块——用 VC6.0 编译的 32 位程序通过 ODBC 驱动连着一个 .mdb 文件。很多人以为这是历史垃圾可现实是工厂产线、医院收费、政务系统里至今还有一大批 32 位老程序靠这条链路在跑没人敢动它们。这篇笔记就把“配置 ODBC 数据源 → 写 VC6.0 连接代码 → 理清 Access 特有的 SQL 规则 → 排掉踩不完的坑”完整走一遍让维护老代码的人少打几个急救电话。2. 为什么老代码偏爱 ODBCVC6.0 时代三种访问技术的取舍标题里的 ODBC、access、vc6.0 三个词放到一起本身就说明了一个时代的选择。2000 年前后要在 VC6.0 里操作 Access 数据库可选的技术不止 ODBC 一种但老工程师最终几乎都选了 ODBC背后有非常现实的原因。2.1 四套并行的访问技术各自的适用面VC6.0 时代能接触到的数据访问方式主要有四种DAO、ADO、OLE DB、ODBC。它们不是互相替代的关系而是层次和封装程度不同。DAO 封装的是 Jet 引擎对着本地 .mdb 和 .xls 操作很顺手单机版 Access 开发确实够用但它的对象模型和 MFC 结合得并不好对 SQL Server 这类远程数据库支持也很弱。ADO 封装的是 OLE DB对象模型友好写起来像 VB但 VC6.0 里用 ADO 要先引入 msado15.tlh 之类的类型库Office 版本一换编译就出乱子。OLE DB 性能最强可它是 COM 接口写起来比 ODBC API 还啰嗦项目的维护门槛高得离谱。ODBC 是数据库厂商联合推的标准 C 接口odbc32.dll 是 Windows 系统组件VC6.0 的 MFC 数据库向导默认支持的就是它。如果你今天做新项目大概率不会选这四样里任何一样。但老工程不行。一台 2003 年出厂的工控机上跑着重型机械管理系统连的是 Access 2000 的 .mdb用 VC6.0 写的 CDatabase 和 CRecordset底层就是 ODBC。这套系统要加功能要让一个懂 ODBC 的工程师接手而不是推倒重来。还有一个很现实的原因Access 数据库文件可以放在网络共享盘上ODBC 驱动负责处理文件共享和锁文件机制DAO 只认本地路径。老系统经常靠这个特性部署在多台工控机上一台机器坏了换一台重新指一下路径就能跑。提示ODBC 不是数据库本身它是统一的数据访问接口。Access 只是它众多数据源中的一种这也是老项目选 ODBC 的另一个动机——哪天把 Access 换成 SQL Server连接串一改上层代码基本不用动。2.2 从 SQLAllocHandle 到 SQLFetchODBC 调用链是固定的六步ODBC API 的执行顺序是固定的我给新手讲时习惯拆成六步分配环境句柄分配连接句柄设置 ODBC 版本协议建立连接分配语句句柄执行 SQL 得到结果。每一步都建立在上一步成功的前提下顺序绝不能乱。典型调用链是SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, henv); SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); SQLAllocHandle(SQL_HANDLE_DBC, henv, hdbc); // 接下来是 SQLDriverConnect 或 SQLConnect // 成功后再分配语句句柄 SQLAllocHandle(SQL_HANDLE_STMT, hdbc, hstmt); // 执行 SQL绑定列抓取结果参数说明SQL_ATTR_ODBC_VERSION 必须显式设置为 SQL_OV_ODBC3否则驱动按 ODBC 2.x 时代的数据类型规则解释碰到日期时间字段会出各种莫名其妙的错误。SQLDriverConnect 和 SQLConnect 的区别在于前者支持完整的连接字符串适合直连后者只认预先配置好的 DSN。这个区别在第 3 章会有更细节的展开。提示老代码里如果出现 SQLAllocEnv、SQLAllocConnect 这种不带 Handle 后缀的函数那是 ODBC 1.x/2.x 的老写法VC6.0 工程里很常见。它们现在还能调通但驱动厂商对新环境下的支持越来越差更新维护时顺手改成三层 Handle 的写法更稳妥。ODBC 之所以让新手觉得黑匣子是因为每层接口都有自己独立的错误记录缓冲区SQLSTATE、NativeError、MessageText 三个字段往往要配合起来看。一个连接失败的错误驱动管理器返回的是一条信息真正的驱动可能已经在下一级诊断里给出了更精确的原因。排查时如果不把每一层的错误都取出来很容易在错误代码上绕圈子。这一点第 6 章会给出一个可用的诊断函数。3. 用 VC6.0 跑通 Access 数据库直连字符串与最小编译参数现在开始搭最小可运行的工程。假设你已经建好了一个 Win32 Console Application接下来要做的只有两件事配置连接方式写出第一段能建表、能插入、能查询的代码。3.1 配置 32 位 DSNSysWOW64 odbcad32 的双通道陷阱教材里教你的流程通常是控制面板 → 管理工具 → 数据源 (ODBC)创建用户 DSN程序里用 SQLConnect。这套流程在 32 位 Windows 上没问题换到 64 位 Windows 就开始埋雷了。64 位系统里存在两个独立的 ODBC 管理器一个是 64 位的放在 C:\Windows\System32\odbcad32.exe另一个是 32 位的放在 C:\Windows\SysWOW64\odbcad32.exe。VC6.0 编译出来的程序是 32 位进程只能加载 32 位驱动能读到的 DSN 也只在 32 位管理器里存在。很多人第一次踩坑都是这个姿势在控制面板打开的 ODBC 管理器里创建了 DSN程序一运行报“未发现数据源名称并且未指定默认驱动程序”。第二个管理器看不见你建的 DSN这不是你的代码错了是系统里两套配置相互隔离。管理器路径服务对象64 位C:\Windows\System32\odbcad32.exe64 位程序如新版 Office、64 位测试工具32 位C:\Windows\SysWOW64\odbcad32.exe32 位程序VC6.0 编译产物配置步骤并不复杂按住 WinR 输入 C:\Windows\SysWOW64\odbcad32.exe 回车切到“系统 DSN”标签页添加选择 Microsoft Access Driver指定 .mdb 文件路径。这样做的好处是旧代码里可以用 SQLConnect 直连但对部署环境要求高换一台机器就必须重配一次。我的习惯是测试阶段用 DSN正式部署全部改成第 3.2 节的直连字符串环境差异越小越省事。3.2 最小可运行代码SQLDriverConnect 直连 Access这段代码的作用是绕开 DSN直接在连接字符串里指定驱动名和数据库文件路径编译后拷到任何装了对应驱动的机器上都能跑。#include windows.h #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 rc; SQLCHAR connOut[256]; SQLSMALLINT connOutLen; // 第一步分配环境句柄 rc SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, henv); if (!SQL_SUCCEEDED(rc)) return -1; // 第二步声明使用 ODBC 3.x 协议 rc SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0); if (!SQL_SUCCEEDED(rc)) return -1; // 第三步分配连接句柄 rc SQLAllocHandle(SQL_HANDLE_DBC, henv, hdbc); if (!SQL_SUCCEEDED(rc)) return -1; // 第四步用完整连接字符串建立连接不依赖预配置的 DSN rc SQLDriverConnect(hdbc, NULL, (SQLCHAR*)DRIVER{Microsoft Access Driver (*.mdb)};DBQD:\\data\\inv.mdb;, SQL_NTS, connOut, sizeof(connOut), connOutLen, SQL_DRIVER_NOPROMPT); if (!SQL_SUCCEEDED(rc)) { printf(connect fail\n); return -1; } // 第五步分配语句句柄 rc SQLAllocHandle(SQL_HANDLE_STMT, hdbc, hstmt); if (!SQL_SUCCEEDED(rc)) return -1; // 第六步执行 SQL。这里建了一张测试表并插入一行 SQLExecDirect(hstmt, (SQLCHAR*)CREATE TABLE test_t(id INT, name TEXT(20)), SQL_NTS); SQLExecDirect(hstmt, (SQLCHAR*)INSERT INTO test_t VALUES(1,Alan), SQL_NTS); // 收尾顺序与分配顺序相反 SQLFreeHandle(SQL_HANDLE_STMT, hstmt); SQLDisconnect(hdbc); SQLFreeHandle(SQL_HANDLE_DBC, hdbc); SQLFreeHandle(SQL_HANDLE_ENV, henv); return 0; }逻辑说明这个程序分解出来就是 2.2 节那六步每调一个接口前先检查返回值。SQLSetEnvAttr 的第三个参数会因为 32/64 位编译环境的指针宽度不同而有差异VC6.0 里写 (SQLPOINTER)SQL_OV_ODBC3 是最安全的写法。SQLDriverConnect 的第五个参数是连接字符串第六个参数 SQL_NTS 表示这个字符串以空字符结尾让驱动自己去量长度。第七、八个参数是输出缓冲区驱动会把实际生效的连接信息回填到这里平时可以不管调试时看一眼能发现驱动做了哪些隐式修正。提示VC6.0 的工程设置里链接器输入中必须加 odbc32.lib否则编译能通过链接阶段会报 unresolved external symbol 之类的一堆错误。MFC 工程如果用了 CDatabase自己也要确保这个库被链接进来。参数说明DBQ 指向 .mdb 文件的绝对路径路径尽量不要带中文和空格。老驱动对 ANSI 路径的处理糙空格会截断中文路径在某些区域设置下会直接连接失败。CREATE TABLE 里的 TEXT(20) 必须带括号TEXT 类型不带长度在老驱动里会被拒绝执行。3.3 MFC 封装CDatabase CRecordset 的最小替换方案老工程里更多见的是 MFC 风格。CDatabase 和 CRecordset 内部封装的就是 ODBC 接口连接字符串和上面 API 的完全一致。CDatabase db; // 关键连接字符串里显式指定驱动和文件路径省去 DSN 配置环节 db.OpenEx(_T(DRIVER{Microsoft Access Driver (*.mdb)};DBQD:\\data\\inv.mdb;), CDatabase::noOdbcDialog); CRecordset rs(db); rs.Open(CRecordset::snapshot, _T(SELECT * FROM test_t WHERE id 1)); while (!rs.IsEOF()) { CString sName; rs.GetFieldValue(_T(name), sName); AfxMessageBox(sName); rs.MoveNext(); } rs.Close(); db.Close();参数说明OpenEx 的第二个参数 CDatabase::noOdbcDialog 很关键它禁止程序在连接失败时弹出 ODBC 配置窗口。产线上的工控机通常无人值守一旦弹出配置框程序会一直挂等在那里看着界面像死了。CRecordset::snapshot 模式适合读小表数据量不大时性能没问题如果多台机器并发读写同一个库用 dynaset 模式更合适它能在读到新数据时自动刷新。GetFieldValue 的字段名对大小写敏感写错不会报错只是返回空字符串排查时很容易忽略。4. ODBC 连接字符串与结果集边界的必调参数ODBC 最容易翻车的地方不在 SQL 语句而在连接字符串的参数。连接字符串看起来就是分号连接的键值对但每个参数背后都有版本和环境的雷。4.1 驱动名与文件格式*.mdb 与 *.accdb 的驱动名不能混写DRIVER 参数写的是注册表里登记的驱动显示名不同版本的 Access 驱动名字不一样。老机器普遍是 Access 2003 及以前驱动名是Microsoft Access Driver (*.mdb)。机器装过 Office 2013 或 Access 2016 运行时后驱动名变成Microsoft Access Driver (*.mdb, *.accdb)。如果你按老文档写死老驱动名在新驱动上反而匹配不到因为驱动注册表项已经改名了。更麻烦的是同一台机器上可能同时存在多个版本的 Access 驱动DRIVER 参数稍微写偏就跑不通。我一般会在程序启动时读一下注册表再拼连接字符串HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\ODBC\ODBCINST.INI\ODBC Drivers里面列出的就是当前机器上所有可用的 ODBC 驱动名。程序启动时遍历这个列表找到一个名字里既包含 Microsoft Access Driver 又包含 accdb 的项拿它的全名去拼连接字符串比写死可靠得多。32 位程序读取时要走 WOW6432Node 分支直接读 Software 分支拿到的是 64 位驱动列表对应关系会错位。4.2 DBQ 路径与扩展参数ExtendedAnsiSQL、MaxBufferSize、SystemDB连接字符串里除了 DRIVER还有几个参数在特定场景下必须显式指定。参数取值示例作用与坑DRIVER{Microsoft Access Driver (*.mdb, *.accdb)}驱动名注册表里叫什么就必须写什么DBQD:\data\inv.mdb 或 \server\share\inv.mdb数据库绝对路径目录必须对该用户可写ExtendedAnsiSQL0 或 1置 1 启用 SQL-92 语义保留字检查更严格MaxBufferSize4096内存缓冲上限32 位进程里调太大反而容易内存不足SystemDB工作组文件绝对路径启用了用户级安全时必须写否则提示非法账户ExtendedAnsiSQL1 是双刃剑。设成 1 后ODBC 驱动改用 Jet 4.0 的 SQL 解析规则支持更多 SQL-92 语法同时把保留字列表也换掉了。老代码里如果有个表叫 USER 或 ORDER设了 1 之后会直接报语法错误设成 0 则维持老行为。我处理老工程时一般默认不碰这个参数除非新版驱动强制要求。SystemDB 属于 Access 工作组安全模型的参数。老系统如果启用了用户/组权限连接字符串里必须显式指定工作组文件路径否则驱动会以默认无权限身份登录返回的错误信息是“非法的账户名或密码”。大多数内部系统为了省事都关了权限真遇到要用的直接补这一项即可。4.3 字符集与参数化查询GBK、UTF-16 与 Access 注入的边界VC6.0 的世界观是 ANSI 里的 GBK而 Access 数据库内部按 UTF-16 存储。ODBC 驱动在拿到你的 ANSI 字符串后会按系统区域设置做一次隐式转码再落库。这带来两个直接后果中文查询条件有时匹配不上长文本字段出现乱码。常见做法是把 Access 字段类型设成“文本”而不是“备注”因为备注字段在老驱动里默认按长文本处理绑定缓冲区时必须显式给够长度。查询条件里的中文串先按 GBK 写进 SQL由驱动自行转换。如果条件是从文件或界面里读出来的要确保它也是 GBK 编码否则转码链路就断了。提示Access 注入这个坎绕不过去。老系统里最典型的表现是把文本框内容直接拼接进 SQL 的 WHERE 子句Access 驱动把参数内容解释成 SQL 执行数据被删改往往神不知鬼不觉。修复方式是参数化查询下面这段是 ODBC API 层面的标准做法// 用问号占位而不是拼字符串 SQLPrepare(hstmt, (SQLCHAR*)SELECT * FROM users WHERE name ?, SQL_NTS); SQLINTEGER cbLen SQL_NTS; SQLCHAR szName[64] 张工; // 绑定参数第 2 个参数是列号第 4 个参数指定 C 语言类型 SQLBindParameter(hstmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR, SQL_CHAR, 64, 0, szName, sizeof(szName), cbLen); SQLExecute(hstmt);参数说明SQLBindParameter 的第 3 个参数 SQL_PARAM_INPUT 表示这是输入参数第 4 个参数 SQL_C_CHAR 说明程序侧的数据类型是 ANSI 字符串第 5 个参数 SQL_CHAR 表示 Access 侧的字段类型第 6 个参数 64 是字段长度必须和表结构里定义的长度一致写大了或写小了都有可能触发截断错误。这样处理后驱动把参数当作纯数据传给 Jet 引擎用户输入再不像 SQL 那样被解析。5. ODBC 访问 Access 常见问题排查五个让老工程师血压升高的现场这一章是重头戏。下面每一条都是从各种工控机、收费终端上总结出来的翻车现场按“现象 → 原因 → 解决”写照着排查能少走一半弯路。5.1 明明配了 DSN程序还是报“未发现数据源名称并且未指定默认驱动程序”现象程序编译正常运行到 SQLConnect 时报错[Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序。用户很委屈说自己明明在控制面板里建了 DSN名字也没拼错。原因控制面板里打开的是 64 位 ODBC 管理器建的 DSN 写进了 64 位注册表视图。而 VC6.0 编译出的程序是 32 位进程读取的是 WOW6432Node 分支的 ODBC 配置两边互相看不见。解决打开 C:\Windows\SysWOW64\odbcad32.exe在系统 DSN 页重新创建一个同名数据源。如果还不行就按第 3.2 节改成 SQLDriverConnect 直连串彻底摆脱 DSN 环境依赖。我评估老项目时有一个习惯DSN 只用于本机调试所有交付代码必须走直连字符串这是生产环境最少复盘成本的做法。5.2 程序一执行就 access violation at address 0x...现象32 位程序在 64 位 Windows 上运行日志里出现access violation at address 0x00403DC5这类崩溃或者弹窗提示“0x77 指令引用的 0x 内存该内存不能为 written”。代码里没有明显越界操作。原因进程是 32 位却加载了 64 位的 Access ODBC 驱动或者反过来。驱动位数不匹配时ODBC 管理器返回的驱动句柄可能是无效指针后续调用走进了一个未初始化的路径。更麻烦的是老驱动在初始化时对系统 DLL 版本有隐式依赖系统补丁更新后驱动加载失败也表现为内存访问违例。解决先在 C:\Windows\SysWOW64\odbcad32.exe 的“驱动程序”页确认有没有 Microsoft Access 开头的 32 位驱动项没有就安装 AccessDatabaseEngine_x86.exe。安装时注意不要选 64 位版VC6.0 程序只能用 32 位驱动。装完把连接字符串里的驱动名更新成新版本驱动名重试。5.3 .mdb 能打开.accdb 连不上现象数据库文件被同事用 Office 2013 另存成了 .accdb 格式老程序立刻连不上。错误信息是“未找到文件”或“不支持该文件格式”。原因.accdb 是 Access 2007 之后引入的格式结构上和 .mdb 不兼容。机器上的 Access 驱动是 2003 时代的只认识 .mdb。VC6.0 程序本身没问题问题出在驱动不认识新格式。解决安装新版 AccessDatabaseEngine_x86.exe驱动名改成Microsoft Access Driver (*.mdb, *.accdb)。注意 32/64 位版本不能混装如果机器上已经装了 64 位 Office再装 32 位引擎会被拒绝需要先卸 Office 或换一台干净机器。老系统能不升格式最好别升.accdb 虽然功能更多但老驱动对它的支持从来就没完整过。5.4 中文查询结果乱码明明存了 20 个字读出来只剩一半现象SQL 查询正常数字字段显示完全正确一碰文本字段中文变成一堆乱码或者字段长度刚好是原来的 50%。原因Access 内部按 UTF-16 存中文ODBC 驱动把它转换成 ANSI 中文要占 2 字节一个字符。SQLBindCol 的第 5 个参数是缓冲区长度单位是字节。如果按“20 个字符”去分配 20 字节的缓冲区驱动只能写进 10 个中文字符多余部分被截断但 ODBC 接口返回 SQL_SUCCESS_WITH_INFO程序不检查这个返回值就继续打印了。解决绑定列之前先取字段的真实字节长度按“字段长度 × 2 2”分配缓冲区或者直接改绑 SQL_C_WCHAR 和宽字符缓冲区这样驱动不做转码字符数和字节数不混淆。必须沿用 ANSI 方案时把缓冲区开到足够大并且每次 SQLFetch 之后检查返回值等于 SQL_SUCCESS_WITH_INFO 时就按截断处理不要直接使用数据。5.5 数据库在共享目录客户端“不能打开数据库”现象程序在本机连自己的 .mdb 完全正常部署到服务器共享目录后报“不能打开数据库”或者“应用程序可能无法识别数据库或文件可能已损坏”。单独用 Access 打开这个文件又是好的。原因Access 联网采用文件共享模式打开数据库时会自动创建一个 .ldb 锁文件写入当前用户和机器名。共享目录对运行程序的用户没有写权限时锁文件创建失败驱动误判数据库文件损坏返回这个极具误导性的错误。解决给运行程序的用户授予共享目录的写权限数据库文件本身要设为可写并且不要在 VSS 快照目录、只读共享目录里放 .mdb。多台机器并发写同一个库时锁文件机制本身就会排队目录权限不对时这个问题会被放大。我对这类部署的建议是把数据库文件单独放在共享盘的一个子目录和程序分发目录分开避免 ACL 权限互相交织。6. 用三个指纹验证 ODBC 连接健康度诊断、超时与压测回归6.1 错误诊断接口要写到程序里ODBC 的错误信息散落在每一级句柄的诊断记录里不取出来就只能靠猜。我习惯把这段代码放进每个项目的公共工具模块里任何 ODBC 调用失败时先打一份诊断记录再看程序逻辑void ShowOdbcError(SQLSMALLINT handleType, SQLHANDLE handle) { SQLCHAR sqlState[8], msg[SQL_MAX_MESSAGE_LENGTH]; SQLINTEGER nativeErr; SQLSMALLINT msgLen; SQLGetDiagRec(handleType, handle, 1, sqlState, nativeErr, msg, sizeof(msg), msgLen); printf(SQLSTATE: %s, Native: %d, Msg: %s\n, sqlState, nativeErr, msg); }调用时机比函数本身更重要。SQLAllocHandle 失败传 SQL_HANDLE_ENV 或 SQL_HANDLE_DBCSQLDriverConnect 失败传 SQL_HANDLE_DBCSQLExecDirect 失败传 SQL_HANDLE_STMT。把失败点的句柄类型传对诊断记录才指向真正的问题。6.2 登录超时与压测回归验证连接是否健康我一般压两道。第一道是连接超时在 SQLDriverConnect 之前执行SQLSetConnectAttr(hdbc, SQL_ATTR_LOGIN_TIMEOUT, (SQLPOINTER)1, 0);把超时压到 1 秒后如果还是连不上基本可以断定是驱动、路径、权限问题而不是网络条件差导致偶发失败。第二道是压测程序里循环 100 次执行SELECT Now()每次记录耗时超过 500 毫秒的调用标红。Access 文件型连接的响应时间波动不会太大如果出现明显毛刺多半是共享目录的 IO 问题或锁冲突而不是 ODBC 驱动本身。我接手老项目时有一个固定顺序先开 SysWOW64 的 odbcad32 确认驱动和 DSN 环境再拼连接字符串最后才动查询代码。顺序一旦乱了多半要从环境排查开始重新走一遍。这套方法论帮我避开了很多冤枉路也希望帮到你。本文还有配套的精品资源点击获取