ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Delphi 通过 mysql.dll 直连 MySQL:老项目迁移与避坑指南

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.dll,64 位配 64 位,混用会在mysql_init处直接崩,且报错信息往往只有一句访问冲突,非常难查。

2.2 为什么不用 ODBC 或 dbExpress

ODBC 需要现场装驱动、配数据源,客户机器上一旦缺驱动,你的程序就起不来;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 下传 nil,client_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返回的每一列都是字符串形式,数值、日期也一样,需要自己按字段类型转换。更麻烦的是 NULL:C 接口里 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 现象:结果集里某列取值时访问冲突

原因:该列为 NULL,row[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,另一个窗体的查询直接崩。后来改成每个需要访问数据库的模块自己持有连接,用完即关,或者用一个连接池统一管理。桌面程序连接开销不大,但句柄生命周期一定要清晰。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进