简介:数据库访问组件是桌面与业务系统开发中连接关系型数据库的关键桥梁,尤其在Delphi生态中,原生高效的数据链路往往决定项目长期稳定性。MyDAC作为一套直接与MySQL协议交互的组件库,通过TMyConnection、TMyQuery等控件实现了握手认证、参数化查询与事务控制。解析v5.10源码,核心价值在于理解连接字符串解析、认证插件匹配、客户端库libmysql.dll动态加载等底层机制,避免因版本或位数不匹配导致的闪退与Lost connection问题。在实际工程中,合理设置字符集、开启协议日志、管理连接池,能显著提升老项目维护效率。本文围绕源文件阅读、最小化连接代码、常见坑位梳理,帮助开发者快速将MyDAC应用至Delphi与MySQL的集成场景。
1. 从绕过官方驱动说起:MyDAC 到底是什么,为什么要读 v5.10 源文件
做 Delphi 连 MySQL 的人迟早会撞上一堵墙:官方驱动版本跟数据库版本经常对不上,换了 MySQL 小版本,连接组件就翻车;DBExpress 的 MySQL 驱动遇到新协议直接黑匣子;用 ODBC 桥接又慢又难调。MyDAC(MySQL Data Access Components)就是为这个场景存在的一套原生组件,它绕过中间层直接用客户端协议跟 MySQL 通信,把 TMyConnection、TMyQuery、TMyScript 这些控件拖进 IDE 就能干活。读 v5.10 源文件这件事,跟用编译好的组件包完全不同:你能看到连接握手、命令编码、结果集解析的具体实现,遇到问题能自己打断点查原因,而不是对着黑匣子猜。适合谁?手上有 Delphi 7 到 XE 时代老项目、需要长期维护 MySQL 连接的开发者,以及想把数据访问层彻底搞懂、不想被驱动绑架的人。
2. 组件骨架:TMyConnection、TMyQuery、TMyScript 的事务边界与连接模型
2.1 连接链路与协议握手
MyDAC 的整套设计都围绕 TMyConnection 展开。这个组件管理的不只是 socket 连接,还包含了握手阶段的状态机、字符集协商、压缩协议开关、SSL 选项这些底层细节。v5.10 的源码里,连接建立过程大致是:解析 Server 属性里的主机名和端口,建立 socket,发送握手包,读取服务器返回的协议版本与能力标志位,再根据 Options 里的配置决定是否启用压缩、是否使用 SSL,最后做认证。这套流程跟 mysql 命令行客户端的差别在于:MyDAC 在每一步之间留了很多回调点,所以你能在 BeforeConnect、AfterConnect 事件里做自定义处理,也可以在源码层直接修改握手行为。
常见做法是直接在 TMyConnection 的配置面板里填 Server、Port、Username、Database,但读源码你会发现,连接串解析逻辑远比面板上的字段丰富。Options 里有几个关键开关直接决定握手能否成功:Compress 控制压缩协议,Protocol 决定走 TCP 还是管道,AuthPlugin 决定认证插件。v5.10 的年代 MySQL 5.0/5.1 还是 mysql_old_password 和 mysql_native_password 混用,所以 AuthPlugin 如果不匹配,握手就会在认证阶段被服务器拒绝,报错往往又含糊,只给一个 Access denied。读源码的好处就是你能在 DoAuth 那个方法上打断点,直接看客户端把什么哈希算法发给了服务器。
// 最小连接配置,Delphi 代码 MyConnection1.Server := '192.168.1.10'; MyConnection1.Port := 3306; MyConnection1.Username := 'app_user'; MyConnection1.Password := 'secret'; MyConnection1.Options.Compress := False; MyConnection1.Options.Protocol := 'TCP/IP'; MyConnection1.Options.AuthPlugin := 'mysql_native_password'; MyConnection1.Open;这段代码里的 AuthPlugin 是 v5.10 里最容易被忽略的选项。如果你连接的 MySQL 是 5.5 以上并启用了新的认证插件,而 MyDAC 这边还按旧协议握手,服务器会直接断开连接,错误信息只看得到 Lost connection。我一般会先把 Protocol 固定为 TCP/IP,确认连接正常后再研究其他传输方式,因为管道和共享内存在不同 Windows 版本上的行为差异很大,容易跟权限问题混在一起。
参数方面,Server 支持主机名或 IP,也可以用冒号带端口,比如 'db01:3307',但显式写 Port 属性更稳妥。Options.Compress 如果打开,传输层会做压缩,对文本类 SQL 效果好,但会额外消耗 CPU;低带宽环境可以开,局域网内建议关掉,不然压出来的延迟反而更高。Charset 参数在 v5.10 里默认是空,但实际连接建立后必须设置,否则中文乱码问题会从握手一路蔓延到结果集编码,后面避坑章节还会专门讲。
2.2 TMyQuery 与 TMyScript 的分工边界
TMyQuery 是最常用的数据集组件,它封装了准备语句、参数绑定、结果集读取三件事。v5.10 的源码里,TMyQuery 对参数的处理不是简单拼接字符串,而是走 PreparedStatement 的协议分支:先给服务器发送 COM_STMT_PREPARE,拿到 statement ID,再用二进制协议绑定参数。这条路径的好处是参数类型可以精确传递,坏处是如果参数类型跟表字段类型不匹配,服务器端会报转换错误。源码里 TMyParam 这个类的 DataType 枚举,控制的就是二进制协议里每个参数的字段类型描述。
TMyScript 是另一套逻辑,它处理的是多条 SQL 的批量执行。常见场景是把建表语句导进来跑:用 TMyScript 可以一次执行整段 DDL,而不需要像 TMyQuery 那样一条条执行。v5.10 的实现里,TMyScript 会按分隔符切分 SQL 语句,逐条发送,遇到错误可以配置是跳过还是中断。这里有个容易踩的坑:TMyScript 默认把分号当作语句分隔符,但如果你的 SQL 里有存储过程定义,过程体内的分号会被误切,解决方案是把存储过程的 DELIMITER 命令在脚本头部声明,或者在源码里改掉语句解析规则。
// 用 TMyScript 批量执行 DDL MyScript1.Connection := MyConnection1; MyScript1.SQL.Text := 'CREATE TABLE IF NOT EXISTS t_user (' + ' id INT PRIMARY KEY AUTO_INCREMENT,' + ' name VARCHAR(50) NOT NULL' + ');' + 'CREATE TABLE IF NOT EXISTS t_order (' + ' id INT PRIMARY KEY AUTO_INCREMENT,' + ' user_id INT NOT NULL' + ');'; MyScript1.Execute;TMyScript .Execute 会遍历整个 SQL 文本,逐段提交给服务器。注意它默认不会把多条语句包在一个事务里,如果中途某条失败,前面执行成功的语句不会自动回滚。我一般会在执行前先开一个显式事务,配合源码里的事务控制方法,保证批量 DDL 的原子性。另外,TMyScript 在解析语句时对注释的处理也比较粗,行注释跟语句分割混在一起容易出现解析错位,所以脚本里尽量少写注释,或者统一用 /* */ 块注释。
2.3 连接池与事务边界
v5.10 的连接池在设计上比新版简单得多,它本质上是一个连接对象的复用列表。源码里 TMyConnection 持有一个连接池管理器,按连接串的哈希值做分组,相同连接串的请求复用同一个池。但这个池是进程内的,不是跨进程的,所以多进程应用里每个进程都会建自己的池,这不算缺陷,但你要有预期。池的大小由 Pooling 和 PoolSize 控制,PoolSize 到了上限后,新请求会排队等待释放,而不是无限新建连接。
连接池还有一个隐蔽行为:空闲连接会被服务器端断开,MySQL 的 wait_timeout 默认 8 小时,但如果你的服务器设置了更短的超时,池里的连接大概率已经死了。MyDAC 的源码在取连接时会发一个 ping 包检测存活,检测失败就丢弃重连。这个检测逻辑在 TMyConnection.Validate 相关方法里,你可以通过 BeforeConnect 事件观察重连频率。事务边界方面,v5.10 的事务是基于连接的,同一个连接上所有数据集共享一个事务,所以如果你开了多个 TMyQuery,它们默认都在同一个事务上下文里。
// 显式事务控制范式 MyConnection1.StartTransaction; try MyQuery1.SQL.Text := 'UPDATE t_account SET balance = balance - 100 WHERE id = :id'; MyQuery1.Params.ParamByName('id').AsInteger := 1001; MyQuery1.Execute; MyQuery2.SQL.Text := 'UPDATE t_account SET balance = balance + 100 WHERE id = :id'; MyQuery2.Params.ParamByName('id').AsInteger := 1002; MyQuery2.Execute; MyConnection1.Commit; except MyConnection1.Rollback; raise; end;事务代码的关键点在于,StartTransaction 调用的是 TMyConnection 的方法,而不是 TMyQuery。v5.10 的 TMyQuery 没有独立事务能力,所有数据集共用连接的事务状态。如果你在不同数据集上交错读写,注意保持事务内语句的顺序一致,避免死锁。还有一个细节:TMyQuery 默认的 FetchAll 是 True,也就是 Execute 之后一次性把结果集全部拉回本地;改成 False 走流式读取,可以在大结果集场景里降低内存占用,但连接会被占住,直到你把结果集读完或关闭数据集。
3. 让 v5.10 在现代环境跑起来的安装与配置
3.1 从源文件到 IDE 安装:包编译与路径
拿到 v5.10 源文件,第一件事不是新建项目,而是把组件包编译进 IDE。源文件里一般带的是 .dpk 工程文件(Delphi Package),你需要根据自己用的 IDE 版本选择对应的包。v5.10 那个年代的源码,官方发布时可能只支持到 Delphi 2007 或 2010,拿到新版 IDE 里编译往往会遇到编译错误,最常见的报错是某个 Winapi 头文件路径找不到,或者 UnicodeString 类型不兼容。我一般是先把所有 .dpk 都打开,逐个编译过去,记录下哪个包报错,再对症处理。
编译顺序有讲究:先编译运行期包(dcl 前缀之外的那个),再编译设计期包(dcl 开头的)。运行期包会被你的项目引用,设计期包负责把组件注册到 IDE 的工具面板上。顺序反了,IDE 会提示找不到运行期包。编译后需要把生成的 .bpl 和 .dcp 文件路径加到 IDE 的 Library path 里,这样每次新建项目就能直接从组件面板拖出 TMyConnection 等控件。这里的坑在于,Library path 有全局和项目两种级别,全局配置对所有项目生效,项目配置只对当前项目生效;如果你多个项目用不同版本的 MyDAC,建议用项目级别的配置,避免版本冲突。
3.2 运行期分发包:libmysql.dll 与客户端库的配合
MyDAC 不是纯 Delphi 实现,它的连接层还需要 MySQL 客户端库的支持。v5.10 源码里有个关键单元处理动态加载 libmysql.dll,程序启动时会按 DllName 属性指定的名字查找这个库。默认是 libmysql.dll,但你要根据 MySQL 服务端版本选择匹配的客户端库版本。MySQL 5.1 之前和 5.5+ 的客户端库在认证协议上有差异,用错版本会出现握手失败或者方法找不到的报错。
运行时部署的常见做法是:把 libmysql.dll 放在可执行文件同目录,或者在系统 PATH 环境变量指向的目录下。放在同目录最稳,因为 MyDAC 的加载逻辑优先找应用程序目录。注意 32 位和 64 位的区分:如果你的 IDE 和项目编译目标是 32 位,就用 32 位客户端库;如果项目是 64 位,就用 64 位库。混用会直接报 "Bad image" 的错误,这个现象在 Windows 上非常典型,很多人误以为是 MyDAC 组件坏了,其实是 DLL 位数不匹配。
3.3 环境变量与 IDE 库路径
如果你的源文件里有源码级别的调试需求,比如想在 TMyConnection 的握手方法里打断点,那就要把源文件目录也加进 IDE 的 Debug path。这样 Delphi 在调试时会自动定位到对应的 .pas 文件,单步跟踪时能看到 MyDAC 内部实现。这个配置跟 Library path 是独立的,不加也能编译,但加了之后调试体验完全不同。我一般会新建一个专门的目录,把源文件复制进去,而不是直接用解压出来的原始目录,因为后面改源码、替换文件时不会污染原始备份。
环境变量方面要注意的是 Windows 的 PATH。有些机器上装了多个 MySQL 工具或客户端,PATH 里的 libmysql.dll 版本可能是被某个应用改写过的。排查问题时先检查 PATH 里所有可能命中的 DLL 路径,再用进程监视工具确认程序启动时实际加载了哪一个。这个问题极其隐蔽,因为程序不报错的时候你根本不会去查加载路径。可以用一个简单的方式验证:在程序启动代码里调用 MyDAC 提供的版本查询接口,打印出实际加载的客户端库版本号,如果跟你预期不一致,再逐步排查加载路径。
// 验证实际加载的客户端库版本 procedure TForm1.Button1Click(Sender: TObject); var LibVer: string; begin // 返回当前动态加载的 libmysql.dll 版本字符串 LibVer := MyConnection1.ClientVersion; ShowMessage(LibVer); end;ClientVersion 属性是 MyDAC 直接问 DLL 导出的版本接口拿到的,跟服务端版本无关。如果这里显示的值跟你放的 DLL 对不上,说明加载路径被 PATH 或系统目录抢先了。解决方法是把目标 DLL 放到 exe 目录,并在代码里用 SetDllDirectory 强制优先搜索该目录,或者把 DllName 属性写成绝对路径。还有一点:v5.10 的 DllName 可以是完整文件名,如果留空,MyDAC 会按一组默认名字逐个尝试加载,这组名字在源码里能看到。
4. 用 TMyConnection + TMyQuery 跑通最小查询:代码与参数
4.1 最小连接代码
跑通 MyDAC 的最小步骤,不是往界面上拖控件,而是先写一段不依赖设计时连接配置的代码。这样你能确定问题出在代码还是环境。连接串相关属性,如 Server、Username、Password、Database,直接在代码里赋值即可。打开连接后,判断 Connected 属性是否为 True。这一步能验证客户端库加载、网络连通、认证三个环节。
program MyDAC_Minimal; uses System.SysUtils, DAcConnection, MyConnection, MyQuery; var Conn: TMyConnection; Q: TMyQuery; begin Conn := TMyConnection.Create(nil); try Conn.Server := '127.0.0.1'; Conn.Port := 3306; Conn.Username := 'root'; Conn.Password := '123456'; Conn.Database := 'testdb'; Conn.Options.Charset := 'utf8mb4'; Conn.Open; WriteLn('Connected: ', Conn.Connected); Q := TMyQuery.Create(nil); try Q.Connection := Conn; Q.SQL.Text := 'SELECT 1 AS val'; Q.Open; WriteLn('Query result: ', Q.FieldByName('val').AsInteger); finally Q.Free; end; finally Conn.Free; end; end.注意这段代码里 Conn.Free 在 Q.Free 之后,因为 Q 还引用着连接对象,顺序反了会访问空指针。这个顺序问题在界面开发里不常见的问题,是因为组件之间有明确的依赖关系。Options.Charset 使用 utf8mb4 而不是老旧的 utf8,可以避免后来的 4 字节 emoji 乱码问题。如果服务端字符集是 latin1,这里设置 utf8mb4 会导致连接后的字符转换错乱,所以 Charset 要跟服务端一致,或者服务端统一改成 utf8mb4。Open 失败时,异常消息里会带出阶段信息:如果是 "Can't connect",说明网络或端口有问题;如果是 "Access denied",说明用户名密码或认证插件有问题。
4.2 参数化查询与类型映射
参数化查询是使用 MyDAC 的核心技能。v5.10 的 TMyQuery 用:ParamName语法标识参数,通过 Params 属性赋值。参数化有两个好处:避免 SQL 注入,以及让 MySQL 服务端复用执行计划。后者在高频调用场景下性能差异明显,尤其当 SQL 文本完全一致、只换参数值时,服务端不需要重新解析 SQL。
// 参数化查询示例 MyQuery.SQL.Text := 'SELECT id, name, balance FROM t_account ' + 'WHERE balance > :min_balance AND status = :st'; MyQuery.Params.ParamByName('min_balance').AsFloat := 1000.0; MyQuery.Params.ParamByName('st').AsString := 'active'; MyQuery.Open;这里有两个容易出错的地方。一是 ParamByName 的参数名必须跟 SQL 里的完全一致,大小写敏感程度取决于源码里的实现,v5.10 一般是严格匹配,写错了会抛 Parameter not found 的异常。二是类型映射:如果字段是 DECIMAL 类型,服务端会返回字符串形式的数值,用 AsFloat 可能会导致精度丢失;正确做法是用 AsBCD 或 AsCurrency 读取。v5.10 源码里对 DECIMAL 的处理是默认映射成字符串字段类型,所以你在 Delphi 里读到的 FieldDefs 类型是 ftString,而不是 ftFloat。不了解这个映射规则的人,常常在读取阶段莫名丢精度。
参数类型还有一个隐蔽坑:当你给参数赋了字符串值,但 SQL 里对应字段是 INT,服务端在二进制协议下会把字符串转成数字,转换失败会报 1366 错误。为了避免这个问题,赋值前要确认参数类型,可以直接指定 MyQuery.Params[0].DataType := ftInteger,或者用 AsInteger 赋值,让组件自动推导。I 在源码里见过不少人在这一块吃了亏,以为参数化就不会有类型问题,其实参数类型跟字段类型不匹配一样报错。
4.3 批量写入与事务提交
批量写入是 MyDAC 在数据导入场景里的强项。v5.10 支持两种方案:一种是用 TMyQuery 循环执行 INSERT 语句,另一种是用 TMyScript 拼接多条语句批量执行。两种方案的性能差异很大:循环执行每条语句都在客户端和服务端之间做一次完整往返,网络开销是瓶颈;TMyScript 把多条语句一次性发送,能显著降低延迟,但语句太长会触发 MySQL 的 max_allowed_packet 限制,超过限制就被直接断开连接。
// 批量导入范式,带事务包裹 MyConnection1.StartTransaction; try MyQuery.SQL.Text := 'INSERT INTO t_log (user_id, action, created_at) ' + 'VALUES (:uid, :act, NOW())'; for i := 0 to 999 do begin MyQuery.Params.ParamByName('uid').AsInteger := userIds[i]; MyQuery.Params.ParamByName('act').AsString := actions[i]; MyQuery.Execute; end; MyConnection1.Commit; except MyConnection1.Rollback; raise; end;这条路径的关键是事务包裹的位置。逐条 Execute 默认是自动提交模式,如果不包事务,每执行一条就提交一次,插入 1000 条就要做 1000 次磁盘刷写,事务把多次写入合并成一次提交,整体耗时能降低 50% 以上。v5.10 的 TMyConnection 在 StartTransaction 之后会把自动提交关掉,Commit 或 Rollback 之后恢复。还有一个容易被忽略的点:事务期间不要执行 DDL,因为 MySQL 会自动隐式提交当前事务,导致前面写的业务数据被提前提交,一旦后面出错回滚也回滚不掉,整个事务的保护就失效了。
MySQL 的 max_allowed_packet 默认值在 v5.1 之前是 1MB,5.5 之后默认 4MB。如果你用 TMyScript 批量导入,要估算单次发送的 SQL 总字节数。有一个简单经验:每条 INSERT 按 200 字节估算,1000 条就是 200KB,远够用;但如果你的行里有 JSON 或 BLOB 字段,单行可能就超过 1MB,那最好改成循环单条执行,或者分批提交。v5.10 源码里 TMyScript 在发送前不会自动拆分超长包,所以在这个问题上没有后悔药,只能调到匹配的值。
5. 避坑:v5.10 的 5 个高发问题
5.1 握手失败:显示 Connected,但一执行 SQL 就报 Lost connection
现象:TMyConnection.Open 成功返回,Connected 为 True,但紧接着 TMyQuery.Open 抛异常,信息是 Lost connection to MySQL server during query。
原因:握手阶段协商成功只代表认证通过,但 SQL 执行时客户端库和服务端的协议版本不匹配。最常见的情况是服务端是 MySQL 8.0+,默认认证插件是 caching_sha2_password,而 MyDAC v5.10 自带的客户端库是老版本 libmysql,只支持 mysql_native_password。握手时服务器允许你连上,但执行查询时客户端库发的能力标志位触发了服务器的不兼容路径,连接就被断掉。
解决:要么把 MySQL 用户认证插件改回 mysql_native_password,执行ALTER USER 'app_user'@'%' IDENTIFIED WITH mysql_native_password BY 'password';,要么找一个支持 caching_sha2_password 的客户端库替换默认的 libmysql.dll。但 v5.10 的年代根本没有这样的库,所以最常见做法是改用户认证插件。如果你不能改服务器配置,那就得考虑升级到新版 MyDAC 或者换官方驱动,v5.10 在 MySQL 8.0 面前确实力不从心。
5.2 libmysql.dll 位数不对:程序启动直接闪退
现象:程序编译成功,双击运行没有任何窗口弹出,Windows 事件日志里记录 0xc000007b 错误。
原因:项目编译目标是 32 位,但你放的 libmysql.dll 是 64 位的,或者反过来。Windows 在加载 DLL 时的位数检查非常严格,一旦不匹配直接拒绝加载,不给你任何异常处理机会。这个报错信息很容易被误判成系统组件损坏,拿去修 VC++ 运行库,浪费时间。
解决:打开项目的目标平台配置,确认是 Win32 还是 Win64,然后下载对应位数的客户端库。放置目录要在 exe 同目录或 PATH 环境下,不要同时放两个版本的 DLL 在搜索路径里。验证方法是用 dumpbin 或任务管理器查看已加载模块,确认进程实际加载的 DLL 路径。
5.3 utf8 乱码:写入正常,读出来变成问号
现象:通过 MyDAC 写入的中文在 mysql 客户端里看到正常,但用 MyDAC 读出来全是问号;或者反过来,客户端写入的 UTF-8 中文在 Delphi 界面上显示乱码。
原因:连接字符集没有设置,或者设置为 utf8 不够全面。MySQL 的 utf8 实际上最多支持 3 字节,真正完整的 UTF-8 是 utf8mb4。此外 v5.10 还需要在 TMyConnection 的 Options.Charset 里指定字符集,这个设置会发送 SET NAMES 语句;如果漏了,连接就用服务端的默认字符集,通常是 latin1 或 utf8,跟客户端的 UnicodeString 编码对不上。
解决:显式设置 Options.Charset := 'utf8mb4',同时确认服务端表结构的字符集也是 utf8mb4。还有一个隐藏点:v5.10 在打开连接时如果没有执行 SET NAMES,后续写入的字符串会按连接建立时的默认字符集编码,导致同样的数据在不同连接上表现不一致。在源码里找到执行 SET NAMES 的位置,确认它是在每次连接握手之后自动调用的,如果没找到,就得自己封装一个打开连接后立即执行的初始化 SQL。
5.4 连接池里的死连接:运行几小时后突然报错
现象:程序运行初期一切正常,跑了几个小时或者过了一夜,某个查询突然报错 "Server has gone away",重启程序后恢复正常。
原因:MySQL 服务器的 wait_timeout 或 interactive_timeout 把空闲连接断开了。MyDAC 连接池里的连接停留在池中,没有活动,服务器超时关闭后,池里的连接对象还是认为自己是有效的,直到下次取用时才发现 socket 已经不可读。v5.10 源码里虽然有 ping 验证,但它只验证 socket 是否能写,而服务端关闭连接后,TCP 层可能不会马上通知客户端,所以 ping 通过但实际执行查询失败。
解决:把池里的连接回收逻辑调短一点。可以在 TMyConnection 的 BeforeConnect 事件里记录连接建立时间,再用定时器定期检查池里的连接是否空闲超过某个阈值,超过就直接关闭并从池里移除。另外把 MySQL 服务端的 wait_timeout 调大到 86400 只能缓解不能根治,关键还是客户端要有重连保护。最直接的做法是执行 SQL 前用 TMyConnection 的 Validate 或 Ping 方法做一次检查,发现连接已死就用新的连接串重新打开。
5.5 源码编译时路径硬编码导致换机器失败
现象:在一台机器上编译好的源码包,拷贝到另一台机器后重新编译,报找不到某个 .dcu 或 .pas 文件,但文件明明就在源码目录里。
原因:v5.10 源码的 .dpk 文件里可能带绝对路径,或者 IDE 的 Search path 配置里引用了原机器的 C 盘路径。Delphi 的包编译在查找单元时按 Library path、Search path、项目文件目录顺序搜索,如果源码相对路径不完整,就会去原机器的绝对路径里找,找不到自然报错。
解决:在 IDE 的 Tools-Options-Library 里把用不到的旧路径全部清掉,只留当前源码目录的相对路径。把所有 .dpk 用文本编辑器打开,检查里面有没有 C:\ 之类的绝对路径指向,有就改成相对路径或者直接删掉。在源码根目录新建一个 LibraryPath.inc 之类的公共配置文件,统一维护路径,这样换机器时只需要改一个文件。
6. 把源文件变成调试工具:协议日志与慢 SQL 定位
MyDAC v5.10 源码里最有价值的部分不是组件本身的业务逻辑,而是那个能开启协议日志的底层单元。TMyConnection 有一个 LogFile 相关的属性,设置后会把客户端和服务端之间的所有协议数据包写入文件。这个功能在生产环境排查问题时极其好用:当你觉得 SQL 正确但结果不对,打开协议日志,能直接看到客户端发出的原始 SQL 字节流和服务端返回的每一段数据。日志里能清晰看到 SQL 是否被正确编码、字符集是否一致、返回结果集的行数是否符合预期。
// 开启协议日志,用于排查一次诡异查询 MyConnection1.LogFile := 'c:\temp\mydac_protocol.log'; MyConnection1.LogEnabled := True; try MyQuery.SQL.Text := 'SELECT * FROM t_order WHERE order_no = :no'; MyQuery.Params.ParamByName('no').AsString := 'ORD-2024-001'; MyQuery.Open; finally MyConnection1.LogEnabled := False; end;定位慢 SQL 是另一个常见用法。MySQL 本身有 slow query log,但它是服务端视角;MyDAC 的日志能看到客户端视角:从发出 SQL 到收到结果集的完整耗时。这个时间包含网络往返、服务端执行、结果集传输三个部分。如果服务端日志显示的慢查询时间短,但我这边总耗时长,问题就在网络或结果集传输上;如果服务端也慢,那就去优化 SQL 本身。这种两边的日志对照,是判断性能瓶颈在哪一段的可靠办法。
日志文件会增长很快,生产环境不要常开,只在排查问题时开启并及时关闭。我个人习惯是在追查一个具体问题时,先把服务端通用日志打开,然后在我的代码里开启 MyDAC 客户端日志,两边时间戳对齐着看,通常半小时内就能定位问题。这个习惯帮我在一个老项目里找出过一次诡异的数据错乱:协议日志显示 MyDAC 发的 SQL 是对的,但结果集的某一行跟数据库里的对不上,最后发现是连接池复用了不同库的会话。这种问题,不借助协议日志几乎没法查。希望这篇整理对你有点用,至少能让你拿到 v5.10 源文件时,知道从哪里下手,不至于被旧组件的各种边界问题劝退。看完觉得有用的话,找台测试机把 MyDAC 装上,跑一遍第 4 章那个最小查询,再对比第 5 章的坑,应该比直接上手老项目省心得多。
本文还有配套的精品资源,点击获取