☰
Delphi dbExpress 连接 MySQL 5.7 的 DLL 部署与排查
2026/10/27 7:58:27 网站建设 项目流程

简介:面向仍使用 Delphi 7、需通过 dbExpress 连接 MySQL 5.7 的开发者,这份压缩包提供可用的 libmysql.dll 与 dbxopenmysql50.dll 动态库,并附带作者亲测通过的连接测试用例,帮助规避同类文件版本不匹配、加载失败或连接不稳定等问题。示例工程兼顾 Windows XP 与 Windows 10,专门演示数据库字段空值与空字符串的判断差异,对旧系统维护、数据校验和兼容性排查具有较实用的参考价值。包内共 17 个文件、约 1.3MB,涵盖 dll 运行库、sql 测试脚本、pas/dpr/dfm 示例源码、exe 可执行程序、docx 配置说明及 dcu、cfg 等编译辅助文件,便于读者直接对照复现。已有 827 人学习,读者可结合示例源码与 TSQLConnection 可视化配置文档梳理连接流程,也能参考发布时仅保留三个运行库的打包思路,减少部署时的遗漏。

1. 一个老项目卡在两个 dll 上:dbExpress 连 MySQL 5.7 的门槛

一个跑了七八年的 Delphi 32 位进销存程序,数据库从旧版本换到 MySQL 5.7,源码一行没动,点开连接就弹Cannot load vendor library。查 SQL、查账号、查防火墙,折腾半天,最后一查是两个 dll 没到位。dbExpress 的结构就是这样:驱动层和客户端库分成两个模块,缺一个整条链路就直接断掉,报错还很含糊。这个压缩包提供的正是配套的一对文件:libmysql.dll和dbxopenmysql50.dll,一个是 MySQL 客户端库,一个是 dbExpress 的驱动实现。适合还在维护 Delphi 老项目、必须对接 MySQL 5.7 的开发者,也适合想把连接过程彻底弄清楚、不再靠反复试错的人。

2. 拆开调用链:dbxopenmysql50 与 libmysql 各自负责哪一段

很多人以为把 dll 丢到目录里就完事了,结果连不上还是一头雾水。先把职责分清楚,后面的路径、位数、版本问题才有判断依据。

2.1 驱动实现做了什么,客户端库做了什么

Delphi 的 dbExpress 是一套薄封装:上层是TSQLConnection、TSQLQuery这些组件,下层通过一个“驱动 dll”把通用调用翻译成具体数据库的协议。dbxopenmysql50.dll就是这个角色,它对外导出getSQLDriverMYSQL这个入口函数,TSQLConnection在Open时会按LibraryName去LoadLibrary,再用GetProcAddress取到入口,拿到一整套函数指针表,之后所有连接、执行、取数都走这张表。

它自己并不实现 MySQL 的网络协议和认证握手,这部分交给libmysql.dll。MySQL 客户端库对外导出的mysql_init、mysql_real_connect、mysql_query、mysql_set_character_set、mysql_real_escape_string这些 C 函数,才是真正跟 3306 端口通信的那一层。所以链路是:

TSQLConnection → dbxopenmysql50.dll → libmysql.dll → MySQL 5.7 服务端

这个分层带来的直接后果是:dbxopenmysql50.dll加载成功不代表能连上,libmysql.dll缺失时,报错往往发生在驱动加载阶段,提示却是“找不到模块”,看上去像驱动没了。搞不清楚这一层,很容易在错误的方向上换 dll 版本。

2.2 位数和版本的两道对齐:dumpbin 五分钟核对法

两道对齐,任何一道错都跑不起来。

第一道是位数。32 位 Delphi 编译出来的 exe 是 32 位进程,只能加载 32 位 dll。如果你的机器上装过 64 位 MySQL 服务端或图形客户端,libmysql.dll很可能是 64 位的,被 PATH 里的目录抢先命中,进程启动阶段直接崩,典型表现是0xc000007b或者“应用程序无法正常启动”。用dumpbin看一眼机器类型,比猜快得多:

:: 在 VS 开发者命令提示符里执行,或者直接用 dumpbin 的完整路径 dumpbin /headers libmysql.dll | findstr /i "machine" dumpbin /headers dbxopenmysql50.dll | findstr /i "machine"

输出里出现14C machine (x86)是 32 位,8664 machine (x64)是 64 位。两个文件必须都是14C,而且要和你的 Delphi 编译目标一致。没有dumpbin的话,用 PowerShell 读 PE 头也行,或者直接看文件大小——32 位的libmysql.dll通常在几 MB 量级,但这不是可靠判断,别拿它当依据。

第二道是版本。MySQL 5.7 的客户端库跟服务端是同源发布的,用它连 5.7 最稳。跨大版本会遇到认证插件不匹配:服务端如果升到了 8.0,默认认证插件换成caching_sha2_password,老客户端库握手时直接报Authentication plugin 'caching_sha2_password' cannot be loaded。反过来,5.7 的库去连 8.0 服务端,同样会在这步失败。

顺便核对一下导出符号是否齐全,防止拿到的是被裁剪过或者改名的版本:

dumpbin /exports libmysql.dll | findstr /i "mysql_init mysql_real_connect mysql_set_character_set"

四个函数都在,说明这是一份完整的客户端库;缺mysql_set_character_set的话,后面字符集设置会失效,中文乱码基本无解。

2.3 加载顺序决定命中哪份 dll

LoadLibrary的搜索顺序里,应用程序所在目录优先级很高,这给了你一个非常实用的手段:把两个 dll 放在 exe 同目录,就能压过 PATH 上那些来路不明的副本。麻烦的是设计期——IDE 里放一个TSQLConnection上去点“测试连接”,加载 dll 的是 IDE 进程,它从自己的 bin 目录找,跟你的 exe 目录无关。于是就出现“设计期能连、编译出来报错”这种看起来像玄学的现象。

定位当前到底命中了哪一份,用where把磁盘上所有同名文件列出来:

where /r C:\ libmysql.dll

如果列出三条以上,而其中一条在某个数据库工具的安装目录下,那基本就是它惹的祸。处理方式不是删别人的文件,而是把自己的那份放到优先级更高的位置(exe 目录),或者干脆在代码里用绝对路径加载,从根上绕开搜索顺序。

3. 部署实施:目录、参数、一段能跑通的最小连接代码

原理清楚之后,落地就三件事:文件放对位置、参数写给驱动、连接代码先做探针再打开。

3.1 文件放哪:exe 目录、IDE bin、系统目录的取舍

压缩包里两个文件的分工和放置位置可以按下面这张表来对照:

文件角色位数要求建议位置
dbxopenmysql50.dlldbExpress 驱动实现,导出getSQLDriverMYSQL与目标进程一致exe 同目录;设计期另放一份到 IDE 的 bin 目录
libmysql.dllMySQL 客户端库,负责协议与认证与驱动 dll 一致与驱动 dll 同目录,靠“同目录优先”保证命中

关于系统目录,我的建议是不要放。System32是 64 位进程的搜索路径,SysWOW64是 32 位进程的,放错一边就等于没放;而且一旦放进去,以后换版本时新旧文件互相覆盖,问题会变得极难复现。exe 同目录是最省心的做法,绿色部署、拷贝即用。

设计期那一份,看你 IDE 的安装位置,通常是bin或bin64子目录。32 位 IDE 对应 32 位 dll,别混。放完重启 IDE,否则已加载的模块不会重新搜索。

3.2 连接参数写在哪:运行时设 Params 与 ini 两条路

dbExpress 的参数有两个来源:一是dbxdrivers.ini/dbxconnections.ini这类配置文件,二是代码里直接给TSQLConnection的属性赋值。发布场景我倾向后者,理由很直接——配置文件的位置在不同 Delphi 版本、不同用户目录下会变,客户机上经常因为找不到 ini 而报“驱动未注册”,排查成本高;写死在代码里,参数跟着 exe 走,行为可预期。

如果确实要用 ini(比如需要让运维改连接串),结构大概是这样,键名以驱动自带的样例为准,不同构建之间可能有出入:

[dbxopenmysql50] DriverName=dbxopenmysql50 GetDriverFunc=getSQLDriverMYSQL LibraryName=dbxopenmysql50.dll VendorLib=libmysql.dll BlobSize=-1 IsolationLevel=ReadCommitted

DriverName是逻辑名,LibraryName是驱动 dll 文件名,VendorLib指向客户端库。三个名字对不上任何一个,都会在Open时抛“找不到厂商库”。

3.3 最小可运行程序:先探针再 Open

下面这段控制台程序是我常用的验证骨架,先手动LoadLibrary确认两个 dll 都能被装载,再去建连接。好处是失败时GetLastError给的错误码比TSQLConnection的提示具体得多,126(找不到模块)、193(不是有效的 Win32 应用)这些码直接指向根因。

program MySQLProbe; {$APPTYPE CONSOLE} uses System.SysUtils, System.Classes, Data.DB, Data.SqlExpr; // 新版为 Data.SqlExpr,老版本单元名可能是 SqlExpr function Probe(const DllName: string): Boolean; var H: THandle; Err: Integer; begin H := LoadLibrary(PChar(DllName)); Result := H <> 0; if Result then FreeLibrary(H) else begin Err := GetLastError; // 126 多为依赖缺失或路径不对;193 基本可以确定是位数不匹配 Writeln(Format('加载 %s 失败,错误码 %d:%s', [DllName, Err, SysErrorMessage(Err)])); end; end; var Conn: TSQLConnection; Q: TSQLQuery; begin // 先探针对端库,再探驱动壳子。顺序反了会掩盖 libmysql 缺失的问题 if not Probe('libmysql.dll') then Exit; if not Probe('dbxopenmysql50.dll') then Exit; Conn := TSQLConnection.Create(nil); Q := TSQLQuery.Create(nil); try Conn.DriverName := 'dbxopenmysql50'; Conn.LibraryName := 'dbxopenmysql50.dll'; Conn.VendorLib := 'libmysql.dll'; Conn.GetDriverFunc := 'getSQLDriverMYSQL'; Conn.LoginPrompt := False; with Conn.Params do begin Values['HostName'] := '127.0.0.1'; // 走 TCP,避免命名管道相关分支 Values['Port'] := '3306'; Values['Database'] := 'demo'; Values['User_Name'] := 'appuser'; // 注意是 User_Name,不是 User Values['Password'] := 'secret'; Values['CharacterSet'] := 'utf8mb4'; // 5.7 支持,四字节字符靠它 Values['BlobSize'] := '-1'; // -1 表示按需取大字段 Values['UseUnicode'] := 'True'; end; Conn.Open; Writeln('连接成功'); Q.SQLConnection := Conn; Q.CommandText := 'SELECT VERSION() AS v, @@character_set_connection AS cs'; Q.Open; Writeln('服务端版本: ', Q.FieldByName('v').AsString); Writeln('连接字符集: ', Q.FieldByName('cs').AsString); Q.Close; finally Q.Free; Conn.Free; end; end.

几处要点值得单独说。User_Name这个键名是 dbExpress 的历史遗留,写成User或UserName都会被忽略,表现为“用户名或密码错误”,很容易误判成权限问题。BlobSize=-1让驱动按实际长度取大字段,设成固定值(比如 256)会把超长文本截断,而且不报错,这种静默截断是最难查的一类问题。CharacterSet在建连后立即生效,比事后执行SET NAMES更早介入,能覆盖驱动内部的元数据查询。

HostName用 IP 而不是localhost,是为了强制走 TCP。某些环境下localhost会触发本地套接字或命名管道分支,而目标驱动对这条路径的支持并不完整。

3.4 用 SQL 侧的证据确认连的是谁

连接成功不等于连对了库。多实例、主从、容器映射端口的环境里,你很可能连到了一台你以为不是的服务器上。三条语句足够自证:

SELECT VERSION() AS server_version, @@hostname AS host_name, @@port AS port, @@character_set_connection AS conn_charset, @@sql_mode AS sql_mode;

@@port和@@hostname能直接戳穿端口映射的问题;@@sql_mode会影响后面聚合查询的行为,先看一眼没坏处。把这几行输出写进日志,以后线上出现数据不一致,翻日志就能确认当时连的是哪个实例。

4. 参数与字符集:把乱码、超时和长连接按住

能连上只是及格,稳定跑起来才算过关。这一章处理三类高频问题:参数写错、字符集链条断裂、长连接被服务端悄悄掐断。

4.1 连接串参数逐项对照

参数作用常见取值写错的后果
HostName服务端地址IP 或域名落到命名管道分支,握手失败
Port监听端口3306 或映射端口连到别的实例,数据写错库
Database默认库名业务库未限定库名的 SQL 全部报错
User_Name账号业务专用账号键名写错被忽略,报密码错
Password口令—同上
CharacterSet连接字符集utf8mb4中文变问号或乱码
BlobSize大字段读取策略-1文本被静默截断
UseUnicode宽字符处理True部分版本下字符串字段异常
Compressed传输压缩False弱网下传输量大,但兼容性优先

Compressed=True在老版本组合上偶发握手异常,除非确实跨广域网,否则保持关闭。参数不是越多越好,每多一个就多一个不确定性来源。

4.2 utf8mb4 与 5.7 的字符集链路

字符集不是一处设置,而是一条链:客户端连接字符集、服务端默认字符集、库和表的字符集、字段的排序规则。任何一环是latin1,中文就会在那一环被替换成问号,而且往往不可逆。

MySQL 5.7 已经完全支持utf8mb4,四字节字符(一些表情符号、生僻字)只有它能存。所以连接参数里直接写utf8mb4,同时确认服务端侧:

SHOW VARIABLES LIKE 'character_set_%'; SHOW VARIABLES LIKE 'collation_%';

character_set_server和character_set_database都应该是utf8mb4。如果服务端是latin1,客户端设成utf8mb4也救不回来——数据在写入时就已经被服务端转换坏了。这种情况需要在服务端配置里改默认字符集并重启,或者建库建表时显式指定:

CREATE DATABASE demo DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;

Delphi 侧还要注意一点:从 XE 版本起字符串默认是 Unicode,读到AnsiString变量里会经过一次隐式转换,转换用的代码页不对就出乱码。统一用string,别在中间环节混用AnsiString,能省掉一大半排查时间。

4.3 长连接、空闲断开与超时的取舍

服务端有个wait_timeout,默认 8 小时左右,空闲超时后连接被单方面关闭。客户端并不知道,下次执行就报MySQL server has gone away(错误码 2006)。桌面程序中午吃饭锁屏两小时,下午回来第一次操作就崩,就是它。

处理方式有三种,按场景选:

  • 短连接:每次操作新建连接、用完即关。实现最简单,但高频操作下建连开销明显,每秒几十次请求的场景不合适。
  • 长连接加心跳:定时执行一条SELECT 1,或者读取Ping类接口,把连接保活。适合连接数不多、操作间隔不规律的桌面端。
  • 长连接加自动重连:捕获 2006 类错误后重建连接并重试一次。要注意事务中间不能盲目重试,否则可能重复提交。

dbExpress 这一层不一定暴露连接超时参数,遇到建连卡死(网络不通、防火墙丢包)的情况,我一般用线程加超时兜底,而不是指望驱动内部有超时。至于查询超时,交给服务端更可控:

SET SESSION MAX_EXECUTION_TIME = 5000;

超过 5 秒的查询被服务端主动中断,比客户端挂在那里等要好。

5. 排查清单:五条踩坑记录,从加载失败到中文乱码

前面讲的是“应该怎样”,这一章讲“实际会怎样”。下面五条都是我在真实项目里撞过的,按现象、原因、解决三段写。

5.1 加载期:三个让人怀疑人生的报错

第一条:Cannot load vendor library [dbxopenmysql50.dll]

现象:程序一启动或者第一次Open就抛这个异常,提示里说找不到指定模块,但文件明明在目录里。

原因:这句话有歧义,它既可能是驱动 dll 本身不在,也可能是驱动 dll 在、但它依赖的libmysql.dll加载失败。Windows 的LoadLibrary在依赖缺失时同样返回“找不到模块”。

解决:用第 3 章那个Probe函数,先探libmysql.dll再探驱动。如果探针报 126,把两个文件放同一目录,用where /r确认没有别的副本抢在前面。这一步能省掉八成“换个 dll 试试”的无用功。

第二条:设计期能连,编译出来的 exe 报同样的错

现象:IDE 里放TSQLConnection点测试连接,正常;一编译运行,立刻失败。

原因:设计期加载 dll 的是 IDE 进程,它从 IDE 的 bin 目录搜索;运行期是 exe 进程,从 exe 目录搜索。两个位置是独立的。

解决:两处都放一份,或者在代码里用ExtractFilePath(ParamStr(0))拼绝对路径加载,彻底摆脱搜索顺序。发布包里只保留 exe 目录那一份。

第三条:0xc000007b,或者“不是有效的 Win32 应用程序”

现象:进程刚起来就崩,连异常对话框都来不及弹。

原因:位数不匹配。最常见的是 PATH 上有个 64 位libmysql.dll(数据库工具安装目录带进来的),被先加载了。

解决:dumpbin /headers逐一核对机器类型;用where /r定位所有副本;把 32 位那份放到 exe 目录,利用“应用目录优先”压过去。别去改系统 PATH,客户机上复现不了。

5.2 运行期:乱码、断连与 sql_mode

第四条:中文变成问号,英文正常

现象:查询结果里中文字段全是?,但用其他客户端工具查同一张表是好的。

原因:连接的字符集是latin1,中文字符在客户端就转换失败了;或者服务端默认字符集是latin1,数据入库时就已经损坏。

解决:连接参数设CharacterSet=utf8mb4,然后执行SELECT @@character_set_connection确认生效;再查character_set_server,如果服务端侧不对,得从服务端配置改起,客户端怎么调都没用。已经写坏的数据只能从源头重导。

第五条:Expression #1 of SELECT list is not in GROUP BY clause(错误码 1055)

现象:同一句 SQL,在旧库上跑得好好的,换 MySQL 5.7 就报错。

原因:5.7 默认开启ONLY_FULL_GROUP_BY,SELECT列表里非聚合列必须出现在GROUP BY中。老代码里大量“随便取一行”的写法全部失效。

解决:优先改 SQL,用ANY_VALUE()显式表达“随便取一个”的意图;实在要临时放开,就调整会话级sql_mode。别图省事直接改全局配置,那样只是把问题推给下一个人。

6. 进阶:把 dll 问题做成启动自检和可回放的日志

上面五条踩坑记录有个共同点:等你看到现象时,现场已经丢了。所以我在交付这类 Delphi 程序时,会强制加一段启动自检,把驱动路径、位数、客户端库版本、服务端版本、字符集全部打出来,一条日志解决过去两小时的对话。

type TMySQLGetClientInfo = function: PAnsiChar; cdecl; function CollectEnvInfo: string; var ExeDir, LibPath: string; HLib: THandle; GetClientInfo: TMySQLGetClientInfo; Buf: array[0..MAX_PATH] of Char; begin ExeDir := IncludeTrailingPathDelimiter(ExtractFilePath(ParamStr(0))); LibPath := ExeDir + 'libmysql.dll'; // 文件是否就位,比 LoadLibrary 的结果更早给出判断 Result := Format('exe=%s; libmysql exists=%s', [ExeDir, BoolToStr(FileExists(LibPath), True)]); HLib := LoadLibrary(PChar(LibPath)); if HLib = 0 then begin Result := Result + Format('; load failed=%d', [GetLastError]); Exit; end; try // 客户端库自报版本,能直接区分拿到的到底是哪一版 GetClientInfo := GetProcAddress(HLib, 'mysql_get_client_info'); if Assigned(GetClientInfo) then Result := Result + '; client=' + string(GetClientInfo()); finally FreeLibrary(HLib); end; // 顺带记录驱动 dll 的真实落点,确认没被 PATH 上的副本截胡 if GetModuleFileName(0, Buf, SizeOf(Buf)) > 0 then Result := Result + '; process=' + string(Buf); end;

这段代码的三个观察点各有用途。FileExists先排除“文件根本没发布”这种低级错误;mysql_get_client_info返回的版本串,能立刻判断拿到的是不是 5.7 那一套;GetModuleFileName记录进程路径,配合前面的where /r结果,可以确认加载的是哪个目录下的副本。

连接成功之后,再补一段服务端侧的证据。用TSQLMonitor把 dbExpress 实际发出的语句记到文件,出问题时不用猜驱动内部做了什么:

var Monitor: TSQLMonitor; begin Monitor := TSQLMonitor.Create(nil); try Monitor.SQLConnection := Conn; Monitor.FileName := ExtractFilePath(ParamStr(0)) + 'dbx_trace.log'; Monitor.TraceList := nil; // 默认记录全部语句 Conn.Open; // ... 业务操作 finally Monitor.Free; end; end;

自检项和处理动作可以固定成一张表,交付前逐项过一遍:

自检项期望值不符时的动作
两个 dll 是否在 exe 目录都存在补文件,不要改 PATH
机器类型是否双 32 位都是 x86换对应位数版本
客户端库版本5.7.x换配套版本
@@character_set_connectionutf8mb4修连接参数或服务端配置
@@sql_mode不含ONLY_FULL_GROUP_BY,或 SQL 已适配改 SQL,别改全局配置

从那以后我每次交付带 dll 的 Delphi 程序,都会在启动时把这五行走一遍,日志落在 exe 旁边,客户发来一句“连不上”时,先让他把那个文件发过来。九成情况看完第一行就知道答案,剩下那一成也能把排查范围从“整个项目”缩到“具体哪一层”。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询