简介:资源面向Delphi开发人员,聚焦如何在不借助OLE DB的前提下,直接通过MySQL官方客户端库mysql.dll完成数据库连接与基本操作,从而省去ADO等中间层的映射开销,更适合关注性能或需要轻量化部署的项目。整个压缩包共53个文件,主要由Delphi单元源码(pas)、工程文件(dpr/dfm/res)、字符集参考文档(html)以及说明文档与许可协议文本组成,压缩后仅87KB,便于快速查阅和分发。包内示例覆盖预编译SQL语句的创建执行、多线程连接的安全使用以及不同Delphi版本的适配工程,同时提供数十种字符集对应的html帮助页,方便解决编码与数据乱码问题。配套动态库交互单元与平台定义头文件封装了函数接口与平台定义,从LoadLibrary、GetProcAddress到mysql_init/mysql_real_connect/mysql_query/mysql_close,能够完整展示直接调用MySQL原生API的调用链与实现思路。已有432人浏览学习,可直接套用或作为自定义数据访问层的起点,适合希望绕开OLE DB中间层、实现轻量高效MySQL访问的Delphi中高级开发者。
1. 老Delphi系统要连MySQL,绕不开这一关:mysql.dll 到底解决了什么
如果你手头有一个用了十年的Delphi客户端,数据库突然要从旧库迁到MySQL,第一反应往往是去装ODBC驱动,或者拖一个dbExpress组件。但我这几年接手的几个内部系统,最后都回到最原始的方案:直接调用 mysql.dll(也就是 libmysql.dll)连接MySQL数据库。这个标题看起来很老,实际在部署、排错、性能控制上都比绕两层封装更可控。适合的读者是维护旧Delphi代码、被第三方驱动版本问题折磨过的开发者;适合的场景是内网环境、客户端机器多、不想每台都装额外驱动。这篇不讲抽象概念,直接讲清楚链路怎么通、参数怎么设、坑在哪。
2. 四条连接路线与DLL选型:为什么先查版本和位数
2.1 四种常见做法:libmysql.dll、ODBC、dbExpress 和第三方组件
Delphi 连接 MySQL 不是只有一种做法,我按实际项目中遇到过的频率排,最常见是下面四条:
| 路线 | 依赖的DLL/驱动 | 适合场景 | 主要缺点 |
|---|---|---|---|
| 直接调用C API | libmysql.dll 或老版本 mysql.dll | 老项目、无驱动环境、要精细控制连接行为 | 要自己声明函数原型,代码量稍大 |
| dbExpress 驱动 | dbxmysql.dll + libmysql.dll | Delphi 自带框架,能配合 TSQLQuery 等组件 | 驱动版本和 Delphi 版本匹配敏感 |
| ODBC 驱动 | myodbc8a.dll / odbcad32 配置 | 临时排查、能容忍安装驱动的环境 | 每台客户端都要装驱动,版本容易乱 |
| 第三方连接组件 | 组件自带动态库 | 不想碰底层细节 | 多一层依赖,出问题不好定位 |
如果你只想快速连一次,ODBC 加 TADOConnection 确实最快。但只要是给多个客户端分发、长时间运行的系统,我建议别把 ODBC 配置写进交付清单,运维很容易把驱动版本装错。标题里直接写“mysql.dll”,说明提问人看到的是老教程或者老代码,那多半走的是第一条路线,也就是直接调用 MySQL 官方客户端动态库,这也是我后面重点展开的部分。
2.2 动手前先对齐三件事:MySQL版本、DLL位数、VC运行库
先别急着写代码。我见过太多连不上的情况,最后发现根本不是代码问题,是三件前置条件没有对齐。
第一,确认服务端 MySQL 的版本大版本,5.5、5.7 和 8.0 在认证插件、字符集默认值上差别很大。比如 MySQL 8.0 默认认证插件是 caching_sha2_password,老版本 libmysql.dll 不认识这个插件,连上去就会报认证错误,这个在第 5 章会单独讲。第二,确认你的 Delphi 编译出的 exe 是 32 位还是 64 位,然后选择对应位数的 libmysql.dll。32 位程序加载 64 位 DLL,LoadLibrary 会直接失败,或者运行到一半崩溃。第三,libmysql.dll 依赖 VC 运行库,老版本依赖 VC2008、VC2010,新版本依赖 VC2015-2022。目标机器上缺运行库,表现就是“找不到指定的模块”,但 DLL 明明放在 exe 同目录。
我的一般做法是:在一台干净测试机上,先把服务端版本、exe 位数、DLL 位数、VC 运行库版本用一个小脚本记下来,再去动代码。这三样对齐,连接链路才谈得上稳定。
2.3 libmysql.dll 和 mysql.dll 的关系:文件名差异为什么重要
标题写的是“mysql.dll”,但 MySQL 官方从 5.0 开始,Windows 客户端库正式文件名就是 libmysql.dll。mysql.dll 是 MySQL 3.x、4.x 时代的老叫法,很多老教程里混着写。实际项目里,我见过有人把 libmysql.dll 改名成 mysql.dll 来兼容旧代码,这种做法能用,但容易让人困惑。
64 位环境下,MySQL 5.x 时代还会看到 libmysql64.dll 这种命名,8.0 之后官方统一叫 libmysql.dll,区分位数靠安装目录而不是文件名。所以当你看到标题“delphi mysql.dll 连接mysql数据库”时,心里要明白:这个 DLL 指的就是 MySQL 官方 C API 客户端库。下面所有代码都按 libmysql.dll 来写,如果你手上只有 mysql.dll,函数导出名是一致的,加载路径换一下即可。
3. 直接调 libmysql.dll 连接:从加载DLL到跑通第一条SELECT
3.1 声明函数指针:只导出你用得上的 C API
直接调用 DLL,第一步是把 C 函数原型翻译成 Delphi 可用的函数指针类型。MySQL C API 有一堆函数,但你日常连接、查询、取结果,其实用不到几个。我一般只声明这一组:
unit MySQLLibIntf; interface uses Windows, SysUtils; type // MYSQL* 和 MYSQL_RES* 在 Delphi 里统一用 Pointer 表示 TMySqlInit = function(MySql: Pointer): Pointer; stdcall; TMySqlRealConnect = function(MySql: Pointer; Host, User, Passwd, Db: PAnsiChar; Port: Cardinal; UnixSocket: PAnsiChar; ClientFlag: Cardinal): Pointer; stdcall; TMySqlQuery = function(MySql: Pointer; Query: PAnsiChar): Integer; stdcall; TMySqlStoreResult = function(MySql: Pointer): Pointer; stdcall; TMySqlFetchRow = function(Result: Pointer): Pointer; stdcall; TMySqlNumFields = function(Result: Pointer): Cardinal; stdcall; TMySqlFreeResult = procedure(Result: Pointer); stdcall; TMySqlClose = procedure(MySql: Pointer); stdcall; TMySqlError = function(MySql: Pointer): PAnsiChar; stdcall; TMySqlAffectedRows = function(MySql: Pointer): Int64; stdcall; TMySqlSetCharacterSet = function(MySql: Pointer; Charset: PAnsiChar): Integer; stdcall; var MySqlInit: TMySqlInit; MySqlRealConnect: TMySqlRealConnect; MySqlQuery: TMySqlQuery; MySqlStoreResult: TMySqlStoreResult; MySqlFetchRow: TMySqlFetchRow; MySqlNumFields: TMySqlNumFields; MySqlFreeResult: TMySqlFreeResult; MySqlClose: TMySqlClose; MySqlError: TMySqlError; MySqlAffectedRows: TMySqlAffectedRows; MySqlSetCharacterSet: TMySqlSetCharacterSet; MySqlDllHandle: HMODULE;上面这些函数指针的类型声明全部使用 stdcall 调用约定,这是 Windows 版 libmysql.dll 的导出约定,用错调用约定程序会直接崩溃。MYSQL* 句柄在 Delphi 里不需要知道内部结构,用 Pointer 就行。这里只声明了 11 个函数,已经覆盖连接、查询、取数、释放的完整链路。如果你还要用事务,额外加 mysql_autocommit、mysql_commit、mysql_rollback 三个,声明方式一样。
3.2 动态加载 DLL:比 static external 多一层可控性
Delphi 里声明 external 'libmysql.dll' 的静态引入写法最省事,但有个问题:DLL 不存在时程序直接弹窗,你连一句自定义错误提示的机会都没有。我一般用 LoadLibrary 动态加载,这样加载失败可以给出明确提示,还能在加载后逐一校验函数地址是否为 nil。
function LoadMySqlDll(const DllName: string): Boolean; begin Result := False; if MySqlDllHandle <> 0 then Exit; // LoadLibrary 返回 0 表示失败 MySqlDllHandle := LoadLibrary(PChar(DllName)); if MySqlDllHandle = 0 then raise Exception.Create('加载 ' + DllName + ' 失败,请检查文件是否存在以及位数是否匹配'); @MySqlInit := GetProcAddress(MySqlDllHandle, 'mysql_init'); @MySqlRealConnect := GetProcAddress(MySqlDllHandle, 'mysql_real_connect'); @MySqlQuery := GetProcAddress(MySqlDllHandle, 'mysql_query'); @MySqlStoreResult := GetProcAddress(MySqlDllHandle, 'mysql_store_result'); @MySqlFetchRow := GetProcAddress(MySqlDllHandle, 'mysql_fetch_row'); @MySqlNumFields := GetProcAddress(MySqlDllHandle, 'mysql_num_fields'); @MySqlFreeResult := GetProcAddress(MySqlDllHandle, 'mysql_free_result'); @MySqlClose := GetProcAddress(MySqlDllHandle, 'mysql_close'); @MySqlError := GetProcAddress(MySqlDllHandle, 'mysql_error'); @MySqlAffectedRows := GetProcAddress(MySqlDllHandle, 'mysql_affected_rows'); @MySqlSetCharacterSet := GetProcAddress(MySqlDllHandle, 'mysql_set_character_set'); // 核心函数只要缺一个,就认为 DLL 不可用 Result := Assigned(MySqlInit) and Assigned(MySqlRealConnect) and Assigned(MySqlQuery); if not Result then begin FreeLibrary(MySqlDllHandle); MySqlDllHandle := 0; end; end;这段代码把函数地址绑定放在一个函数里,加载失败时能精准抛出原因。注意赋值函数指针用了 @MySqlInit := GetProcAddress(...) 的形式,在 Delphi 7 到 XE 各个版本下都通用。参数 DllName 建议传绝对路径,比如 exe 同目录下的 libmysql.dll,不要只传文件名依赖系统 PATH。加载成功后再用 TMySqlInit 这类函数指针调用接口,后续代码就和静态 external 没有区别。
3.3 建立连接与设置字符集:mysql_real_connect 的正确姿势
加载完 DLL,下一步就是初始化连接句柄并连接服务端。连接函数这块有几个细节容易踩:第一个参数是 mysql_init 返回的句柄,不是 nil;连接失败时错误信息要立刻拷贝出来,因为 mysql_error 返回的指针指向 DLL 内部缓冲,下一次调用就会覆盖。
function ConnectMySql(const Host, User, Passwd, DbName: string; Port: Integer): Pointer; var MySql: Pointer; UTF8Host, UTF8User, UTF8Passwd, UTF8Db: UTF8String; CharsetName: UTF8String; begin // 第一步:初始化 MYSQL 对象 MySql := MySqlInit(nil); if MySql = nil then raise Exception.Create('mysql_init 失败'); try // 第二步:显式设置字符集,避免中文乱码 CharsetName := UTF8String('utf8mb4'); MySqlSetCharacterSet(MySql, PAnsiChar(CharsetName)); // 第三步:把 Delphi string 转成 UTF-8 UTF8Host := UTF8String(Host); UTF8User := UTF8String(User); UTF8Passwd := UTF8String(Passwd); UTF8Db := UTF8String(DbName); // 第四步:建立连接 Result := MySqlRealConnect(MySql, PAnsiChar(UTF8Host), PAnsiChar(UTF8User), PAnsiChar(UTF8Passwd), PAnsiChar(UTF8Db), Port, nil, 0); if Result = nil then raise Exception.Create('连接 MySQL 失败: ' + string(AnsiString(MySqlError(MySql)))); except // 连接失败也必须释放句柄,否则泄漏 MySqlClose(MySql); raise; end; end;这里最关键的参数是 port 和 clientflag。clientflag 传 0 表示使用默认行为,如果你需要多语句执行,可以传 CLIENT_MULTI_STATEMENTS,这个常量值为 65536,需要自己定义。UnixSocket 参数在 Windows 下传 nil。注意字符集设置必须在 mysql_real_connect 之前调用,否则连接建立后还要额外发一条 SET NAMES,多一次往返。连接失败时,先读错误信息再调用 MySqlClose,顺序不能反。
3.4 执行查询和读取结果:mysql_store_result 与遍历行集
连接建立后,执行 SQL 和读取结果集是用得最频繁的部分。mysql_query 返回 0 表示成功,非 0 表示失败;SELECT 类语句需要用 mysql_store_result 把结果拉到客户端,再逐行遍历。
function QueryAndDump(MySql: Pointer; const Sql: string): Integer; var UTF8Sql: UTF8String; Res: Pointer; Row: Pointer; RowData: ^PAnsiChar; // MYSQL_ROW 本质是 char** FieldCount: Cardinal; I: Integer; FieldPtr: PAnsiChar; begin // SQL 统一转 UTF-8 UTF8Sql := UTF8String(Sql); if MySqlQuery(MySql, PAnsiChar(UTF8Sql)) <> 0 then raise Exception.Create('SQL 执行失败: ' + string(AnsiString(MySqlError(MySql)))); // 取结果集 Res := MySqlStoreResult(MySql); if Res = nil then begin // 如果没有结果集(比如 UPDATE),返回受影响行数 Result := Integer(MySqlAffectedRows(MySql)); Exit; end; try FieldCount := MySqlNumFields(Res); // 循环取行,mysql_fetch_row 返回 nil 表示取完 while True do begin Row := MySqlFetchRow(Res); if Row = nil then Break; // Row 指向一个 PAnsiChar 数组,长度为 FieldCount RowData := Pointer(Row); for I := 0 to FieldCount - 1 do begin FieldPtr := PAnsiChar(NativeUInt(RowData) + I * SizeOf(PAnsiChar)); if PAnsiChar(FieldPtr^) = nil then Write('NULL') else Write(UTF8ToString(RawByteString(FieldPtr^))); if I < FieldCount - 1 then Write(#9); end; WriteLn; end; Result := 0; finally // 无论是否取完,结果集必须释放 MySqlFreeResult(Res); end; end;这段代码有两个容易出错的地方:一个是 MYSQL_ROW 的内存布局是连续排列的 PAnsiChar 指针数组,不是二维数组,所以取第 I 个字段要用指针偏移计算;另一个是字段值本身是 PAnsiChar,指向的数据是 UTF-8 编码,转成 Delphi string 时要用 UTF8ToString,不能直接赋值给 AnsiString,否则中文会乱。mysql_store_result 之后,结果集内存必须由 mysql_free_result 释放,这个一定要放在 try/finally 里兜底。
3.5 释放连接与内存:try/finally 兜底
上面两段代码已经展示了释放逻辑,这里把原则单独强调一下。MySQL C API 里凡是返回 MYSQL_RES 的函数,拿到手之后都必须释放;MYSQL* 句柄也一样,用完必须 mysql_close。我见过一个系统运行半个月后连接数打满,查到最后就是有个分支忘记 close。
释放的固定写法是:
procedure SafeCloseMySql(var MySql: Pointer); begin if MySql <> nil then begin MySqlClose(MySql); MySql := nil; end; end;把释放动作和置 nil 组合成一个过程,每次用完连接后调用,就不会出现重复释放的问题。mysql_close 对已经 close 过的指针再调一次,在部分版本里会崩溃,所以置 nil 这步不能省。
4. 不想手写C API就用 dbExpress:dbxmysql.dll 连线与部署
4.1 TSQLConnection 最小配置:DriverName 与两个 DLL 的关系
如果你的 Delphi 版本较新,或者项目里已经大量使用 DataSet 组件,手写 C API 会显得格格不入。这时常见做法是用 dbExpress 框架,连接组件是 TSQLConnection,它背后需要两个 DLL:一个是框架驱动 dbxmysql.dll,另一个还是 MySQL 客户端库 libmysql.dll。这两个 DLL 分工不同,驱动负责把 dbExpress 的调用翻译成 MySQL API 调用,libmysql.dll 负责实际网络通信。很多人只拷了 dbxmysql.dll 就报错,就是因为 vendor lib 没带。
var Conn: TSQLConnection; begin Conn := TSQLConnection.Create(nil); try Conn.DriverName := 'MySQL'; Conn.LibraryName := 'dbxmysql.dll'; Conn.GetDriverFunc := 'getSQLDriverMYSQL'; Conn.VendorLib := 'libmysql.dll'; Conn.Params.Values['HostName'] := '10.0.1.20'; Conn.Params.Values['Database'] := 'erp'; Conn.Params.Values['User_Name'] := 'app_user'; Conn.Params.Values['Password'] := 'app_pass'; Conn.Params.Values['Port'] := '3306'; Conn.Params.Values['BlobSize'] := '-1'; Conn.Connected := True; // 连接成功后可以操作 TSQLQuery / TSQLDataSet finally Conn.Free; end; end;这里最关键的是四个属性:DriverName 决定走哪种驱动;LibraryName 是 Delphi 端加载的驱动 DLL;GetDriverFunc 是驱动入口函数名;VendorLib 是底层客户端库。驱动名 'MySQL' 是 dbExpress 内置的映射名字,不同 Delphi 版本映射的文件名不一样,Delphi 7 时代叫 dbexpmysql.dll,2007 之后普遍叫 dbxmysql.dll。所以看到老教程写 dbexpmysql.dll 别奇怪,先确认你的版本。
4.2 驱动文件的版本匹配和部署顺序
dbExpress 的坑主要集中在这几个文件之间的版本匹配。驱动 dll 和 Delphi 版本绑定,比如 Delphi 2010 的驱动不能拿去给 Delphi XE 用;vendor lib 和服务端 MySQL 版本建议同大版本,至少客户端库不能比服务端老太多。部署到客户机时,我的习惯是把 dbxmysql.dll 和 libmysql.dll 统一放到 exe 同目录,不要让驱动去系统目录找。
如果连接时报“Cannot find dbxmysql.dll”或者“无法定位程序输入点”,按这个顺序排查:第一,exe 同目录下是不是两个 dll 都在;第二,位数是不是一致;第三,驱动 dll 是不是当前 Delphi 版本自带的;第四,libmysql.dll 有没有依赖 VC 运行库。dbExpress 这块没有太多玄学,版本对齐了基本一次通。
4.3 两条路线的边界:什么时候回到 mysql.dll
dbExpress 看起来省事,但它有个隐蔽问题:调试不方便。TSQLConnection 内部会缓存元数据、管理事务,出错时错误信息经过多层包装,你很难判断到底是 SQL 问题、驱动问题还是网络问题。另外 dbExpress 对 MySQL 的字符集处理依赖驱动版本,老版本经常出现 utf8mb4 支持不全,导致 emoji 字符写入报错。
所以我的边界判断标准是:新项目、用 DataSet 组件多、团队熟悉 dbExpress,走驱动路线;老项目、只要增删改查、要明确掌控连接行为,回到 C API 路线。标题既然落在 mysql.dll 上,说明提问场景大概率是后者。两条路线不是替代关系,而是同一层 API 的两种封装方式,底层都是 libmysql.dll 在干活。
5. 连接 MySQL 的 5 个经典翻车现场:排查顺序与后悔药
5.1 现象:程序启动即报“无法找到指定的模块”
这个错误最常见,程序双击打开直接弹窗,提示“无法找到指定的模块”,或者“无法定位程序输入点”。现象发生在 LoadLibrary 之前,说明 DLL 没找到或者 DLL 自身依赖的库缺失。原因往往不是 libmysql.dll 不在,而是它依赖的 msvcr100.dll、libssl.dll 不在,或系统 PATH 里没有。解决方法是把 libmysql.dll 放到 exe 同目录,并把用到的 VC 运行库一起带上;如果用了 LoadLibrary 动态加载,就能把这段错误捕获并给出中文提示,而不是让系统弹窗。
5.2 现象:连接 MySQL 8.0 时报认证插件错误
连接时报错类似 “Authentication plugin 'caching_sha2_password' cannot be loaded”。原因是你手上的 libmysql.dll 版本太老,只认识 mysql_native_password,而 MySQL 8.0 默认认证插件是 caching_sha2_password。解决有两个方向:升级 libmysql.dll 到 8.0 配套版本,这是最稳妥的;如果服务端无法升级,可以在 MySQL 里把账号改回老认证方式,执行 ALTER USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY '密码'; 前者治本,后者治标。我一般优先升级 DLL,因为老认证方式在后续 MySQL 版本里会被移除。
5.3 现象:32位/64位不一致导致间歇性崩溃
程序能启动,连接偶尔成功,但运行一会儿崩溃,或者 LoadLibrary 时好时坏。这种最容易迷惑人,因为错误不固定。原因就是 exe 位数和 DLL 位数不一致,Windows 对 64 位进程加载 32 位 DLL 会直接拒绝,但 32 位进程加载 64 位 DLL 在某些老版本 Windows 上不会立刻失败,而是调用时内存错乱。解决方法是确认 Delphi 编译目标平台。Delphi 7 到 XE 默认只有 32 位,那就找 32 位 libmysql.dll;用 XE2 以后编译 64 位,就配 64 位客户端库。别靠猜,用 dumpbin 或任务管理器确认进程位数。
5.4 现象:中文数据写入变问号、读出乱码
数据写入后变成 ????,或者读出来一片乱码。原因通常是两个:一个是字符集没有对齐,服务端、连接、客户端三层各说各话;另一个是 Delphi 版本差异,Delphi 7 的 string 是 AnsiString,Delphi 2009 之后 string 是 UnicodeString,直接把 string 塞给 PAnsiChar 参数会得到错误字节。解决方法是连接后统一字符集,服务端用 utf8mb4,代码里用 mysql_set_character_set 设置连接字符集;传参时先用 UTF8String 转换,取结果时用 UTF8ToString 还原。这一条做到位,乱码问题基本消失。
5.5 现象:连接一段时间后 MySQL 报 too many connections
系统跑几天后,MySQL 端报 “Too many connections”,重启程序又正常。原因是程序里的连接没释放,或者长连接被网络设备断开但客户端不知道。排查时先执行 SHOW PROCESSLIST,看大量 Sleep 状态的连接堆在哪里,定位是哪个模块漏调了 mysql_close。解决是每一条连接路径都用 try/finally 保证释放,长连接加心跳,定期执行 SELECT 1;另外可以在 mysql_options 里设置 MYSQL_OPT_RECONNECT,让断掉的长连接自动重连,但要注意这个选项只对网络中断有效,服务端主动 kill 连接时它不会自动补偿。
6. 把连接封装进公共单元:一个线程一个句柄,别偷懒
前面讲的都是裸调用,项目里如果每个窗体都写一遍 LoadLibrary 和 mysql_query,后面维护会很难受。我一般会把连接句柄和操作方法收进一个简单的连接类,让业务代码只关心 SQL 字符串。
type TMySqlConn = class private FHandle: Pointer; public constructor Create(const Host, User, Passwd, DbName: string; Port: Integer); destructor Destroy; override; function ExecSql(const ASql: string): Int64; function Ping: Boolean; end; constructor TMySqlConn.Create(const Host, User, Passwd, DbName: string; Port: Integer); begin FHandle := ConnectMySql(Host, User, Passwd, DbName, Port); end; destructor TMySqlConn.Destroy; begin if FHandle <> nil then MySqlClose(FHandle); inherited; end; function TMySqlConn.Ping: Boolean; begin Result := MySqlPing(FHandle) = 0; end;封装之后,有三件事比封装本身更重要。第一,一个连接句柄只属于一个线程,不要在全局公共连接上做并发查询,MySQL C API 的连接句柄不是线程安全的,多线程并发需要每线程独立连接,只有 libmysql.dll 这个库本身是线程安全的。第二,长连接一定要有心跳,建议每 30 秒执行一次 SELECT 1,超时时间根据网络环境调整,否则路由器或防火墙会把空闲连接清掉。第三,所有 SQL 和错误信息要打日志,连接失败时把 mysql_error 的原文记录下来,光记一个异常类名没法定位问题。
我在这类项目上翻过的车,最后基本都归结成三个字:版、位、码——版本、位数、字符集。把这三样先对齐,链路就成功了一半,剩下的一半在释放和日志。这套连接方案我沿用到现在,虽然代码不花哨,但胜在每一步都可控。希望帮到你。
本文还有配套的精品资源,点击获取