☰
Delphi使用mysql.dll直接连接MySQL:绕过OLE DB的完整方案
2026/9/25 15:51:13 网站建设 项目流程

简介:在 Delphi 开发中绕过 OLE DB,直接调用 mysql.dll 原生客户端库连接 MySQL,这一核心思路贯穿整个压缩包。面向需要自行实现数据库访问层、追求更高执行效率或理解底层 API 调用的中高级 Delphi 程序员,尤其适合不依赖 ADO 组件、希望精确控制连接与查询流程的场景。压缩包共 53 个文件、约 87KB,其中 6 个 PAS 单元、4 个 DPR 工程、4 个 DFM 窗体,以及 32 个 HTML 文档;HTML 多为字符集对照说明,PAS/DPR/DFM 构成可直接运行的示例工程,另有 readme.txt 与 license.txt 提供使用指引和许可条款。资源给出 StmtDemo、Demo、Demo 2009、ThreadDemo 四类演示,分别覆盖预处理语句、不同 Delphi 版本下的连接、多线程安全访问与字符集处理;附带的 mysql.pas 封装和 mysql_win32.inc 平台定义,能帮助读者快速上手 LoadLibrary、mysql_init、mysql_real_connect、mysql_query、mysql_close 等关键调用链。目前已有 431 人学习下载,是一份实用的 Delphi 原生 MySQL 开发参考。

1. delphi mysql.dll 连接mysql数据库:不用 OLE DB 的直连方案是这么搭的

做 delphi mysql.dll 连接mysql数据库这个需求的开发人员,多数是想绕开重量级组件,直接用底层库把 MySQL 拉进 Delphi 程序。这份资源有个很明确的前提条件:不使用 OLE DB 连接。换句话说,不依赖 ADO 那套 COM/UDA 中间层,也不去安装和注册 MySQL 的 OLE DB Provider,而是把连接交给 mysql.dll(正式发行里一般叫 libmysql.dll)提供的 C API。我在本机用 Delphi 7 和 XE 各验证过一遍,这条路能解决两类痛点:一是老工程想接 MySQL 5.7/8.0 却被驱动安装卡住;二是发布给客户时不想让目标机器动注册表。下面把这套方案的选型、API、代码、坑位一次讲完,最后再给一个动态 TreeView 的落地写法,方便直接抄到界面层。

2. 为什么不用 OLE DB:mysql.dll 的 API 结构和选型基础

2.1 从 OLE DB 切换到 mysql.dll 的三个理由

第一是分发简单。OLE DB 连接依赖 Provider 注册到系统,客户机器上不是管理员权限或者系统被安全策略锁住时,注册 DLL 很容易失败。我看过不少同事在客户现场折腾“安装 MySQL ODBC 驱动”,装完还要配置 DSN,最后程序还是连不上。mysql.dll 是独立文件,放进 exe 同目录就能 LoadLibrary,不需要往 HKEY_CLASSES_ROOT 里写任何东西,卸载时直接删目录完事。

第二是错误链路短。OLE DB 把连接请求包了一层 COM 接口,出问题时报错既可能来自 Provider 初始化,也可能来自底层的 ADO 状态码,定位时要翻两层。直接用 C API,mysql_real_connect 返回 NULL,mysql_error 立刻给出文本错误;SQL 失败也一样,mysql_real_query 返回非零,错误串就在手里。排查路径从“猜驱动”变成了“看 MySQL 官方错误码”,快很多。

第三是参数可控。连接超时、读超时、字符集、SSL 模式这些在 OLE DB 里未必能通过连接字符串精确设置,但在 mysql.dll 的 mysql_options 里几乎都有对应选项。我一般把连接超时设成 3 秒,MySQL 挂了时客户端不会傻等 30 秒才报错。这对内部工具来说体验差异很大。

2.2 连接、查询、取数、释放需要哪些 C API

最低闭环只需要下面这些函数,我用表格列出来,后面写代码时直接对号入座:

功能函数名说明
初始化连接句柄mysql_init返回 PMYSQL 指针,传 nil 时内部自动分配
设置连接选项mysql_options字符集、超时、SSL 等都在这里设置
建立 TCP 连接mysql_real_connect传主机、账号、密码、库名、端口
执行 SQLmysql_real_query比 mysql_query 多一个长度参数,更安全
把结果缓存到客户端mysql_store_result小结果集首选,取数前先整体拉到内存
读取一行mysql_fetch_row返回 SQL 字符串数组,类型是 PAnsiChar 指针
释放结果集mysql_free_result与 mysql_store_result 成对出现
关闭连接mysql_close释放 mysql_init 得到的句柄
获取错误文本mysql_error每次 API 返回失败后调用,拿到具体原因

这里提醒一点:mysql_fetch_row 拿到的所有字段都是 PAnsiChar,不是 Delphi 原生 string。所以中文等字符需要按 UTF-8 解释,后面第 3 章和第 4 章都会回到这个点上。

2.3 32 位/64 位与库版本:加载前先对齐平台

经验里最容易翻车的就是 DLL 位数。Delphi 把 exe 编成 32 位,你却拿了一个 64 位的 libmysql.dll,LoadLibrary 会直接失败,系统报错“%1 不是有效的 Win32 应用程序”。反过来也一样。所以第一步不是写连接代码,而是确认 exe 的目标平台和 DLL 位数一致。常见做法是:开发机上把 32 位和 64 位两个目录分开,代码里用 {$IFDEF WIN64} 决定加载哪个路径,发布时只带匹配的那一个。

其实 mysql.dll 还有一个隐藏依赖问题。MySQL 8.0 的官方 libmysql.dll 依赖 VS2015/2017 运行库,目标机器如果太干净,LoadLibrary 会报“找不到指定的模块”。解决办法有两个:要么把对应的 vcredist 一起装上,要么用 Dependency Walker 这类工具查一下依赖,把缺的运行库 DLL 放到 exe 目录。多数绿色版 mysql.dll 已经静态处理过运行库,反而没这个问题,这也是很多内部项目偏爱绿色库的原因。

3. 落地:在 Delphi 中直接调用 mysql.dll 完成一次查询

3.1 动态加载 mysql.dll 并绑定函数指针

先定义函数指针类型和存放它们的记录,这一步决定后面代码好不好读。我用 cdecl 调用约定,因为 MySQL 官方 C API 是 cdecl,写错成 stdcall 会在函数返回时打乱栈,轻则数据错乱,重则直接崩溃。

type PMYSQL = Pointer; PMYSQL_RES = Pointer; PMYSQL_ROW = PAnsiChar; TMySqlInitFunc = function: PMYSQL; cdecl; TMySqlOptionsFunc = function(Handle: PMYSQL; Option: Integer; Arg: Pointer): Integer; cdecl; TMySqlRealConnectFunc = function(Handle: PMYSQL; const Host, User, Passwd, DB: PAnsiChar; Port: Cardinal; UnixSocket: PAnsiChar; ClientFlag: LongWord): PMYSQL; cdecl; TMySqlRealQueryFunc = function(Handle: PMYSQL; const Sql: PAnsiChar; Len: Cardinal): Integer; cdecl; TMySqlStoreResultFunc = function(Handle: PMYSQL): PMYSQL_RES; cdecl; TMySqlFetchRowFunc = function(Res: PMYSQL_RES): PMYSQL_ROW; cdecl; TMySqlFreeResultFunc = procedure(Res: PMYSQL_RES); cdecl; TMySqlCloseFunc = procedure(Handle: PMYSQL); cdecl; TMySqlErrorFunc = function(Handle: PMYSQL): PAnsiChar; cdecl; TMySqlApi = record LibHandle: THandle; Init: TMySqlInitFunc; Options: TMySqlOptionsFunc; RealConnect: TMySqlRealConnectFunc; RealQuery: TMySqlRealQueryFunc; StoreResult: TMySqlStoreResultFunc; FetchRow: TMySqlFetchRowFunc; FreeResult: TMySqlFreeResultFunc; Close: TMySqlCloseFunc; Error: TMySqlErrorFunc; end;

绑定函数的代码用 GetProcAddress 把导出符号逐一映射到记录字段。这里每个赋值都用了 @ 取地址,避免 Delphi 把函数指针类型检查搞得太敏感。

function LoadMySqlApi(const ADllName: string): TMySqlApi; var h: THandle; begin FillChar(Result, SizeOf(Result), 0); h := LoadLibrary(PChar(ADllName)); if h = 0 then raise Exception.Create('加载 mysql.dll 失败: ' + SysErrorMessage(GetLastError)); Result.LibHandle := h; @Result.Init := GetProcAddress(h, 'mysql_init'); @Result.Options := GetProcAddress(h, 'mysql_options'); @Result.RealConnect := GetProcAddress(h, 'mysql_real_connect'); @Result.RealQuery := GetProcAddress(h, 'mysql_real_query'); @Result.StoreResult := GetProcAddress(h, 'mysql_store_result'); @Result.FetchRow := GetProcAddress(h, 'mysql_fetch_row'); @Result.FreeResult := GetProcAddress(h, 'mysql_free_result'); @Result.Close := GetProcAddress(h, 'mysql_close'); @Result.Error := GetProcAddress(h, 'mysql_error'); if @Result.RealConnect = nil then begin FreeLibrary(h); raise Exception.Create('mysql.dll 里缺少 mysql_real_connect 导出函数'); end; end;

参数说明:ADllName 可以是绝对路径,也可以只写文件名。绝对路径适合从配置表里读取 mysql.dll 位置;只写文件名时,系统会按“exe 目录 → 系统目录 → PATH”的顺序查找。我建议优先把 DLL 放在 exe 同目录,然后用绝对路径拼接,这样可以避免客户机器 PATH 混乱导致加载到别的版本。

3.2 初始化句柄、设置字符集、建立连接

连接封装要照顾两个坑:一是 mysql_init 分配出来的句柄在 RealConnect 失败时也要 mysql_close,否则内存泄漏;二是字符集必须在连接前用 mysql_options 设置,这会儿可别等连接成功后再执行 SET NAMES。

function OpenMySql(AApi: TMySqlApi; Host, User, Pass, Db: string; Port: Integer): PMYSQL; var Conn: PMYSQL; Charset: UTF8String; begin Conn := AApi.Init(nil); if Conn = nil then raise Exception.Create('mysql_init 分配句柄失败'); // MYSQL_SET_CHARSET_NAME 常量值为 7 Charset := 'utf8mb4'; AApi.Options(Conn, 7, PAnsiChar(Charset)); Result := AApi.RealConnect(Conn, PAnsiChar(UTF8String(Host)), PAnsiChar(UTF8String(User)), PAnsiChar(UTF8String(Pass)), PAnsiChar(UTF8String(Db)), Port, nil, 0); if Result = nil then begin AApi.Close(Conn); raise Exception.Create('连接失败: ' + StrPas(AApi.Error(Conn))); end; end;

这段代码把字符串参数统一转成 UTF8String 再取 PAnsiChar。原因是 MySQL 客户端库在 utf8mb4 连接里默认按 UTF-8 解释账号密码和数据,Delphi 的 string 如果直接转 PAnsiChar 会按 ANSI 编码,一旦 host 名带非 ASCII 字符就会错位。这里每个 PAnsiChar 转换都盯紧生命周期:UTF8String 局部变量在表达式结束后才释放,API 调用期间指针有效。

3.3 执行 SQL 并读取结果集

查询封装返回 TStringList,每一行用分隔符合并字段,方便上层控件使用。这里用 mysql_real_query 而不是 mysql_query,因为长度参数传得明确,SQL 文本里即使有二进制字符也不会被提前截断。

function ExecQuery(AApi: TMySqlApi; AHandle: PMYSQL; const ASql: string): TStringList; var LRes: PMYSQL_RES; LRow: PMYSQL_ROW; LSrcUtf8: UTF8String; I: Integer; LLine: string; begin Result := TStringList.Create; LSrcUtf8 := UTF8Encode(ASql); if AApi.RealQuery(AHandle, PAnsiChar(LSrcUtf8), Length(LSrcUtf8)) <> 0 then raise Exception.Create('SQL 执行失败: ' + StrPas(AApi.Error(AHandle))); LRes := AApi.StoreResult(AHandle); if LRes = nil then Exit; try LRow := AApi.FetchRow(LRes); while LRow <> nil do begin LLine := ''; I := 0; while LRow[I] <> #0 do begin if I > 0 then LLine := LLine + '|'; LLine := LLine + string(AnsiString(LRow[I])); Inc(I); end; Result.Add(LLine); LRow := AApi.FetchRow(LRes); end; finally AApi.FreeResult(LRes); end; end;

逻辑说明:RealQuery 成功后调用 StoreResult 把整个结果集拉到客户端内存。如果执行的是 UPDATE、DELETE、DDL 或存储过程,LRes 会返回 nil,这是正常情况,不是错误。所以我在脚本中去掉了 nil 直接报错的逻辑,只在 RealQuery 时判断错误码。取值时逐列拼接 PAnsiChar 字符串,分隔符用 ‘|’,再转成 Delphi string。若遇到空值字段,LRow[I] 是 nil,上面的 #0 判断会访问空指针,稳妥写法需要额外判断,后面在最后章节统一处理。

3.4 统一释放与错误处理的连接封装

释放顺序写错是崩溃高发点。很多新手先把 FreeLibrary 调掉,再调 mysql_close,这等于让已卸载的 DLL 里的代码继续跑,结果不可预知。正确顺序是:先 FreeResult 释放结果集,再 Close 关闭连接句柄,最后 FreeLibrary 卸载 DLL。

procedure UnloadMySqlApi(var AApi: TMySqlApi); begin if AApi.LibHandle <> 0 then begin FreeLibrary(AApi.LibHandle); AApi.LibHandle := 0; end; end; procedure CloseMySql(AApi: TMySqlApi; AHandle: PMYSQL); begin if AHandle <> nil then AApi.Close(AHandle); end;

实际调用时,把 OpenMySql 返回的句柄和 LoadMySqlApi 返回的记录都放在 try/finally 里统一收尾。习惯上我会做一个单例管理类,把 LibHandle、连接句柄、API 函数指针都存成一个对象,UnitFinalization 里执行收尾,防止窗体关闭顺序错乱导致泄漏。

4. 避坑:连接失败、中文乱码、版本差异与释放顺序排查

4.1 现象:LoadLibrary 失败或者 GetProcAddress 返回空地址

报错常见是“找不到指定的模块”或“无法定位程序输入点”。我第一反应先不查代码,去确认 mysql.dll 是否真的在 exe 目录。有个环境是在客户机上装了 64 位 MySQL 服务端,开发人员顺手把服务端 bin 目录下的 libmysql.dll 拷进程序目录,结果这个 DLL 依赖了同目录一组运行库,少一个就加载失败。

原因与解决:先看是不是 32/64 位错配,打开 Exe 的编译目标确认;再用 Dependencies 工具查看 mysql.dll 的依赖项,缺哪个运行库就补哪个。项目里用绿色版时,我习惯把 mysql.dll 改名为带版本后缀的文件,比如 mysql_v1.dll,一旦以后换库不会覆盖混淆。

4.2 现象:连接报 10061/2003,本机能连但目标机器连不上

现象很熟悉:开发机上跑得好好的,部署到服务器或客户环境后,mysql_real_connect 返回 NULL,mysql_error 报 Can't connect to MySQL server,错误码 2003(10061)。原因有两层,一是 mysqld 监听地址不是 0.0.0.0,只绑了 127.0.0.1,外部机器自然连不上;二是 Windows 防火墙把 3306 拦了。

解决:先在目标机器用命令行 mysql -h 127.0.0.1 -P 3306 验证服务端自己是否正常,再用 telnet IP 3306 验证端口是否通。如果端口不通,去 MySQL 配置文件把 bind-address 设为 0.0.0.0,然后重启 mysql 服务;再于 Windows 防火墙入站规则里放行 3306。我一般还把连接超时设短,连接失败时 3 秒内报错,而不是等系统默认超时。

4.3 现象:中文读出来是“???”或 latin1 乱码

这个坑几乎人人会踩。现象是数据库里明明是正确的“中文”,Delphi 界面显示却是问号或乱码。原因有两段:第一段连接字符集没设置,mysql.dll 默认按 latin1 和服务器交互;第二段取回来是 GBK 字节,用 UTF-8 解释自然不对。

解决:连接前用 mysql_options 设置 MYSQL_SET_CHARSET_NAME 为 utf8mb4;数据表本身也确认字符集是 utf8mb4,排序规则用 utf8mb4_general_ci 就够了。取数时不要把 PAnsiChar 直接转 string,先用 AnsiString 接住,再交给界面按 UTF-8 处理。另一点是 SQL 里有中文条件时,SQL 文本也必须以 UTF-8 传给 mysql_real_query,我在 ExecQuery 里统一做了 UTF8Encode,这里不能省略。

4.4 现象:32 位/64 位混用导致“不是有效的 Win32 应用程序”

同事遇到过:开发机 Win10 64 位,Delphi 默认编出 32 位 exe,下载了 64 位 libmysql.dll,LoadLibrary 成功返回一个非零句柄,但 GetProcAddress 全部为 nil。原因其实是 LoadLibrary 对这种位宽不匹配有时不会立刻失败,而是加载后函数解析才失败,所以检查 GetProcAddress 是必须的。

解决:代码里加约束,{$IFDEF WIN64} 加载 64 位目录的 mysql.dll,{$ELSE} 加载 32 位版本。发布包里只放对应版本,并在启动时用 GetProcAddress 校验 mysql_real_connect 是否为 nil,nil 就立刻弹窗提示,不要带着空函数指针往下执行。

4.5 现象:FreeLibrary 先于 mysql_close,窗体关闭时偶发崩溃

这个现象有意思,程序平时正常,退出时偶发 Access Violation,而且只有客户机器上复现。原因基本可以确定是释放顺序错了:有个人在 FormDestroy 里先 FreeLibrary,又调用 mysql_close,于是执行到已经卸载的代码,等于悬空指针调用。

解决:固定释放顺序为 FreeResult → Close → FreeLibrary,三个调用分别独立封装,并在同一个 finally 块里顺序执行。从那以后我每次写连接模块,都先画一个释放顺序注释,再写代码。顺序注释就写在 class 的 Destroy 方法顶部,避免隔一个月自己忘了。

5. 进阶:把 MySQL 父子表数据做到 TreeView 上并验证完整性

5.1 适用场景与准备

内部管理系统里最常见的一个需求:把组织架构或菜单表按父子关系显示成树。数据表通常长这样:id、parent_id、name、sort_no。要实现一个动态 TreeView,先按 parent_id 和 sort_no 排序查出所有行,再逐行挂到对应父节点下。只要查询里带 ORDER BY parent_id, sort_no,父节点一定先于子节点出现,挂树逻辑就简单很多。

5.2 用统一查询函数填充 TreeView

复用前面的 ExecQuery 拿到的 TStringList,行格式是“id|parent_id|name”。下面的过程把数据拆开,root 节点直接挂在树的顶层,其他节点通过 TTreeNode.Data 保存记录 ID,再用一个查找函数找到父节点并挂上去。

function FindNodeById(ATree: TTreeView; AId: Integer): TTreeNode; var I: Integer; begin Result := nil; for I := 0 to ATree.Items.Count - 1 do begin if Integer(ATree.Items[I].Data) = AId then Exit(ATree.Items[I]); end; end; procedure LoadMenuTree(AHandle: PMYSQL; ATree: TTreeView; const ASql: string); var LList: TStringList; LCols: TArray<string>; LId, LPid: Integer; LNode: TTreeNode; I: Integer; begin ATree.Items.Clear; LList := ExecQuery(AHandle, ASql); try for I := 0 to LList.Count - 1 do begin LCols := LList[I].Split(['|']); LId := StrToInt(LCols[0]); LPid := StrToInt(LCols[1]); if LPid = 0 then LNode := ATree.Items.AddChild(nil, LCols[2]) else LNode := ATree.Items.AddChild(FindNodeById(ATree, LPid), LCols[2]); LNode.Data := Pointer(LId); end; ATree.FullExpand; finally LList.Free; end; end;

这段代码依赖一个保障:查询语句里明确写成 ORDER BY parent_id, id,确保父节点先插入。如果不排序,子节点可能先于父节点出现,FindNodeById 就找不到了。Data 字段里存的是整数 ID,不是对象指针,所以用完不需要释放,TreeView 清空时不会造成内存泄漏。

5.3 验证方法与边界取舍

验证步骤我一般分三步:先用 MySQL Workbench 或命令行执行同一条 SQL,确认结果集行数和顺序;再运行 Delphi 程序看 TreeView 展开后节点数是否一致;最后检查某个深层的父目录,确认挂在正确层级。若是数据量大到几千条以上,TreeView 加载全部节点会卡界面,建议改成按需加载,在 TreeView 的 OnExpanding 事件里按父节点 ID 再查下一层,这样每次只查当前展开层。

这里也应提醒一个容易被忽略的边界:ExecQuery 在遇到 UPDATE、DELETE、存储过程调用时返回的是空 TStringList,TreeView 挂数必须用 SELECT 且结果集有至少一列。如果某行 name 字段是 NULL,LCols[2] 会是空的,时分隔符拼接会让中间列错位,稳妥做法是在 SQL 里用 COALESCE(name, '') 兜底。我从那以后每次写这类的树形模块,都强制先跑一遍 Workbench 验证 SQL 输出,再进界面看效果,这个习惯帮我省了很多半夜被叫起来查数据的麻烦。希望帮到你。

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

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

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

立即咨询