Delphi使用mysql.dll直接连接MySQL:绕过OLE DB的完整方案
发布时间:2026/9/25 15:51:19来源:尧图网络
简介在 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 DBmysql.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 APImysql_real_connect 返回 NULLmysql_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.dllLoadLibrary 会直接失败系统报错“%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 返回 NULLmysql_error 报 Cant 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.dllLoadLibrary 成功返回一个非零句柄但 GetProcAddress 全部为 nil。原因其实是 LoadLibrary 对这种位宽不匹配有时不会立刻失败而是加载后函数解析才失败所以检查 GetProcAddress 是必须的。解决代码里加约束{$IFDEF WIN64} 加载 64 位目录的 mysql.dll{$ELSE} 加载 32 位版本。发布包里只放对应版本并在启动时用 GetProcAddress 校验 mysql_real_connect 是否为 nilnil 就立刻弹窗提示不要带着空函数指针往下执行。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: TArraystring; 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、存储过程调用时返回的是空 TStringListTreeView 挂数必须用 SELECT 且结果集有至少一列。如果某行 name 字段是 NULLLCols[2] 会是空的时分隔符拼接会让中间列错位稳妥做法是在 SQL 里用 COALESCE(name, ) 兜底。我从那以后每次写这类的树形模块都强制先跑一遍 Workbench 验证 SQL 输出再进界面看效果这个习惯帮我省了很多半夜被叫起来查数据的麻烦。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网