Delphi 通过 mysql.dll 直连 MySQL:老项目迁移与避坑指南 简介这份资源面向使用Delphi进行数据库开发的程序员聚焦于绕过OLE DB、直接调用mysql.dll原生客户端库来连接MySQL数据库这一技术路线。包内共53个文件以html字符集说明文档、pas单元文件、dpr工程文件、dfm窗体文件及res资源文件为主压缩包约87KB体积轻巧便于快速查阅。内容涵盖语句执行、多线程连接、字符集处理等示例工程并附有mysql.pas接口封装单元与mysql_win32.inc平台定义文件readme与license则提供使用说明和授权条款。目前已有431人学习下载。读者可从中获得一套完整的原生API调用范例理解从加载动态库、获取函数指针到初始化连接、执行SQL、关闭释放的完整链路同时借鉴多线程安全与字符集转换的实践思路适合希望摆脱中间层依赖、追求性能与跨平台能力的Delphi开发者参考。1. Delphi 用 mysql.dll 连 MySQL为什么老项目还在走这条路接手一个十几年前的 Delphi 桌面项目客户要求把数据从本地 Paradox 迁到 MySQL你打开工程一看数据访问层全是TQuery加 BDE服务器上还躺着一个mysql.dll。这时候最稳的做法不是推翻重来而是让 Delphi 通过mysql.dll直连 MySQL。这条路听起来老但在工控、收银、库存这类长期维护的桌面系统里它依然是成本最低、改动最小、现场最容易部署的方案。核心思路就一句话把 MySQL 官方的 C 客户端动态库当成一个普通 DLL用 Delphi 的external声明把几个关键函数绑进来自己封装一层连接、查询、取值。适合谁适合手里有存量 Delphi 工程、不想引入大型驱动框架、又需要快速对接 MySQL 的开发者。下面把我实际落地时踩过的路完整讲一遍。2. 先搞清楚 mysql.dll 到底是什么选型与依赖关系2.1 libmysql.dll 和 mysql.dll 不是一回事很多人一上来就搜「mysql.dll 下载」结果下到的文件放到程序目录里LoadLibrary直接返回 0。这里必须先厘清MySQL 官方 C 客户端库在 Windows 上的文件名是libmysql.dll它依赖libcrypto、libssl之类的加密库而网上流传的所谓mysql.dll有的是第三方重新打包的有的是旧版本改名还有的干脆是别的数据库的驱动。Delphi 里用external声明时写的是文件名不是逻辑名所以文件名必须和磁盘上完全一致。我一般会这样做从 MySQL 官方安装包的lib目录里取libmysql.dll连同它依赖的 DLL 一起放到 exe 同级目录。判断一个 DLL 能不能用最直接的办法是用dumpbin /exports libmysql.dll看导出表里有没有mysql_init、mysql_real_connect、mysql_query、mysql_store_result、mysql_fetch_row这几个符号。如果导出表里全是 C 修饰名那说明拿到的是 C 接口版本Delphi 直接调会很痛苦得换 C 接口版本。提示32 位 Delphi 必须配 32 位 libmysql.dll64 位配 64 位混用会在mysql_init处直接崩且报错信息往往只有一句访问冲突非常难查。2.2 为什么不用 ODBC 或 dbExpressODBC 需要现场装驱动、配数据源客户机器上一旦缺驱动你的程序就起不来dbExpress 的 MySQL 驱动在较新 Delphi 版本里支持有限老版本又依赖特定版本的libmysql.dll版本对不上照样翻车。直接调mysql.dll的好处是依赖最少一个 DLL 加几个声明程序自带不依赖系统数据源。代价是你要自己管连接、管结果集、管字符集工作量比用现成组件大但可控性最强。对于要发给几百个门店、现场没人会配环境的场景这个代价是值得的。2.3 连接字符串里最容易忽略的三个参数连接 MySQL 时mysql_real_connect的参数顺序是 host、user、passwd、db、port、unix_socket、client_flag。前五个好理解unix_socket在 Windows 下传 nilclient_flag才是关键。我一般会带上CLIENT_MULTI_STATEMENTS方便一次执行多条语句如果表里有中文还要确保连接后立刻执行SET NAMES utf8mb4否则取出来全是问号。端口默认 3306但现场经常被改成 3307 或别的所以端口一定要做成可配置项不要写死。3. 用 Delphi 声明并调用 mysql.dll 的最小可运行代码3.1 函数声明与类型映射Delphi 调 C 接口最麻烦的是类型对应。MYSQL是一个不透明结构体Delphi 里用一个Pointer或record占位即可不需要知道内部布局。MYSQL_RES同理。MYSQL_ROW在 C 里是char**Delphi 里映射成PPAnsiChar。下面是我实际用的声明片段放在一个独立单元里避免和业务代码混在一起。unit uMySQLApi; interface const // 客户端标志按需组合 CLIENT_MULTI_STATEMENTS 65536; type PMYSQL Pointer; PMYSQL_RES Pointer; PMYSQL_ROW ^PAnsiChar; // 注意stdcall 还是 cdecl 取决于 DLL 编译方式官方 libmysql.dll 是 cdecl function mysql_init(mysql: PMYSQL): PMYSQL; cdecl; external libmysql.dll; function mysql_real_connect(mysql: PMYSQL; const host, user, passwd, db: PAnsiChar; port: LongWord; const unix_socket: PAnsiChar; client_flag: LongWord): PMYSQL; cdecl; external libmysql.dll; function mysql_query(mysql: PMYSQL; const q: PAnsiChar): Integer; cdecl; external libmysql.dll; function mysql_store_result(mysql: PMYSQL): PMYSQL_RES; cdecl; external libmysql.dll; function mysql_fetch_row(result: PMYSQL_RES): PMYSQL_ROW; cdecl; external libmysql.dll; function mysql_num_fields(result: PMYSQL_RES): LongWord; cdecl; external libmysql.dll; procedure mysql_free_result(result: PMYSQL_RES); cdecl; external libmysql.dll; procedure mysql_close(mysql: PMYSQL); cdecl; external libmysql.dll; function mysql_error(mysql: PMYSQL): PAnsiChar; cdecl; external libmysql.dll; implementation end.逻辑说明cdecl是 C 默认调用约定官方 DLL 基本都是这个写成stdcall会导致栈不平衡程序在调用后随机崩溃。PAnsiChar对应 C 的char*Delphi 2009 以后字符串默认是 Unicode传参前必须转成AnsiString再取指针否则中文和特殊字符会乱。mysql_init传 nil 表示让库自己分配一个MYSQL结构返回值就是句柄。3.2 建立连接并执行第一条查询有了声明连接本身只有几行。关键是每一步都要判空并且把mysql_error的内容记下来否则出问题只能看到「连接失败」四个字。var conn: PMYSQL; res: PMYSQL_RES; row: PMYSQL_ROW; sHost, sUser, sPass, sDb: AnsiString; begin sHost : 127.0.0.1; sUser : root; sPass : yourpass; sDb : testdb; conn : mysql_init(nil); if conn nil then begin // 初始化失败通常是 DLL 没加载成功 Exit; end; if mysql_real_connect(conn, PAnsiChar(sHost), PAnsiChar(sUser), PAnsiChar(sPass), PAnsiChar(sDb), 3306, nil, CLIENT_MULTI_STATEMENTS) nil then begin // 这里一定要取错误信息否则不知道是账号错还是库不存在 raise Exception.Create(连接失败: string(mysql_error(conn))); end; // 设置字符集必须在查询前执行 mysql_query(conn, SET NAMES utf8mb4); if mysql_query(conn, SELECT id, name FROM users LIMIT 10) 0 then begin raise Exception.Create(查询失败: string(mysql_error(conn))); end; res : mysql_store_result(conn); if res nil then begin while True do begin row : mysql_fetch_row(res); if row nil then Break; // row[0] 是第一列row[1] 是第二列按字段顺序取 // 这里用 OutputDebugString 或写日志不要直接弹窗 end; mysql_free_result(res); end; mysql_close(conn); end;逻辑说明mysql_store_result把整个结果集拉到本地内存适合结果集不大的场景如果查询返回几十万行应该改用mysql_use_result逐行读但那样连接在读完前不能执行别的查询。mysql_fetch_row返回 nil 表示读完返回的row是char**每个元素是 C 字符串Delphi 里用string(AnsiString(row[0]))转成 Unicode 字符串。参数方面CLIENT_MULTI_STATEMENTS允许一次发多条 SQL但要注意注入风险拼接 SQL 时务必转义。3.3 把连接封装成类避免每次重写直接在主窗体里写上面那段代码项目一大就失控。我一般会封一个TMySQLConn类把连接、查询、取值、释放都收进去业务层只调方法。类里维护一个PMYSQL句柄析构时确保mysql_close被调用。查询方法返回一个二维数组或TStringList让上层不用碰指针。这样即使以后换成别的驱动改动也只在这一个单元里。4. 参数怎么设、结果怎么取几个必须调对的细节4.1 字符集与中文乱码的根因中文乱码几乎全出在字符集上。MySQL 服务端、连接、表、字段四层字符集要一致最省事的做法是建库建表都用utf8mb4连接后立刻SET NAMES utf8mb4。Delphi 这边AnsiString在传参时按当前代码页编码如果代码页和 MySQL 期望的不一致就会乱。稳妥做法是显式用UTF8Encode把 Unicode 字符串转成 UTF-8 字节流再传给mysql_query。取结果时row里的字节流按 UTF-8 解码回 Unicode。这一步不做中文名、地址、备注字段迟早出问题。4.2 端口、超时与重连mysql_real_connect本身没有超时参数连接超时由底层 socket 决定现场网络差时可能卡很久。我一般会在连接前用mysql_options设置MYSQL_OPT_CONNECT_TIMEOUT单位是秒。这个函数也要声明进来。另外长时间空闲的连接会被服务端断开执行查询时返回「MySQL server has gone away」。解决办法是捕获这个错误后自动重连一次而不是直接报错给用户。重连前要确保旧句柄已经mysql_close否则句柄泄漏。4.3 结果集字段类型与 NULL 处理mysql_fetch_row返回的每一列都是字符串形式数值、日期也一样需要自己按字段类型转换。更麻烦的是 NULLC 接口里 NULL 列在row中对应的是空指针直接string(row[i])会崩。正确做法是先用mysql_fetch_lengths拿到每列长度再判断指针是否为空。如果为空赋默认值不为空按长度构造字符串。这个坑我在一个报表模块里踩过某条记录的备注为 NULL整个查询循环直接访问冲突查了半天才发现是空指针。5. 避坑与排查mysql.dll 连接最常见的五类翻车5.1 现象程序启动就报「找不到 libmysql.dll」原因DLL 不在 exe 同级目录也不在系统 PATH 里。Delphi 的external是运行时加载编译能过运行才报。解决把libmysql.dll及其依赖的加密库一起复制到 exe 目录或者用SetDllDirectory在启动时指定目录。不要指望开发机上有、客户机上也有。5.2 现象调用mysql_real_connect后进程直接退出原因调用约定写错或者 32/64 位不匹配。cdecl写成stdcall栈在调用后被错误清理表现为随机崩溃或静默退出。解决确认 DLL 位数和 Delphi 目标平台一致确认声明用cdecl。可以用dumpbin /headers看 DLL 是 32 位还是 64 位。5.3 现象查询返回中文全是问号原因连接字符集没设或者 Delphi 传参时用了错误的编码。解决连接后立即SET NAMES utf8mb4传参前用UTF8Encode转字节流取结果后按 UTF-8 解码。三处缺一不可。5.4 现象执行一段时间后报「MySQL server has gone away」原因连接空闲超时被服务端断开或者单次发送的 SQL 超过max_allowed_packet。解决捕获错误码 2006 和 2013自动重连大字段写入时分批或者调大服务端max_allowed_packet。重连后要重新执行SET NAMES。5.5 现象结果集里某列取值时访问冲突原因该列为 NULLrow[i]是空指针直接转字符串导致。解决用mysql_fetch_lengths判断或者先判断指针是否为 nil。所有从row取值的代码都要加这层保护不能假设每列都有值。6. 进阶把 mysql.dll 封装成可替换的数据访问层走到这一步连接已经能跑通但真正决定这个方案能不能长期维护的是封装方式。我的习惯是定义一个接口IDataAccess里面只有Connect、Query、Execute、Disconnect四个方法TMySQLAccess实现它内部持有PMYSQL。业务代码只依赖接口不依赖具体 DLL。这样以后要换回 ODBC 或者换别的数据库只加一个新实现类业务层一行不改。验证封装是否到位有个简单办法把libmysql.dll临时改名程序启动时应该给出明确的「数据库驱动加载失败」提示而不是崩溃。这要求你在mysql_init返回 nil 时就有兜底逻辑。另一个验证点是并发桌面程序一般单连接就够但如果后台线程也要查库每个线程必须用自己的PMYSQL句柄C 接口的连接不是线程安全的共用一个句柄会出现结果集错乱。参数配置上我一般把 host、port、user、pass、db、charset 放在一个 ini 文件里程序启动时读连接失败时把mysql_error的内容写进日志文件而不是弹窗。现场维护人员看不懂英文错误但日志能发回来。日志里要包含时间、错误码、错误信息、当前执行的 SQL 前 200 个字符这三样足够定位九成问题。最后说个我自己的教训早期图省事把连接句柄放在全局变量里多个窗体共用结果一个窗体关闭时调了mysql_close另一个窗体的查询直接崩。后来改成每个需要访问数据库的模块自己持有连接用完即关或者用一个连接池统一管理。桌面程序连接开销不大但句柄生命周期一定要清晰。希望帮到你。本文还有配套的精品资源点击获取