Delphi老项目数据库层拆解:MyDAC v5.10源码精读与二次开发实践

发布时间:2026/10/27 11:04:17
Delphi老项目数据库层拆解:MyDAC v5.10源码精读与二次开发实践

简介:MySQL Data Access Components(MyDAC)v5.10 源文件面向使用 Delphi 7 至 2007 及 C++Builder 的数据库开发者,用于在不依赖第三方库或中间件的前提下直接连接 MySQL 服务器,完成数据读取、写入、更新与删除等操作。组件集成了事务处理、批处理、存储过程、异步操作与错误处理机制,兼顾高性能与低资源占用,适合构建数据交互频繁的桌面应用。压缩包共 1403 个文件,约 1.39MB,以 pas 源文件、dpk 与 dpr 工程文件、dfm 窗体文件、res 资源文件及 cfg 配置为主,另含帮助文档、许可协议与编译运行说明,便于按模块查阅与二次定制。已有 168 人学习下载。源文件形式让开发者可深入组件内部实现,按项目需求调整连接逻辑与功能扩展,是 Delphi/C++Builder 环境下 MySQL 开发的一份实用参考。

1. 一套 Delphi 老项目的数据库层,为什么值得单独拆开看

接手一个跑了七八年的 Delphi 客户端时,最怕的不是界面乱,而是数据访问层像黑匣子:连接串散落在十几个窗体里,SQL 拼接靠字符串加法,换一台机器就报驱动版本不对。这时候翻到 MyDAC v5.10 的源文件,会有种“终于能看见底层”的感觉。它是 Devart 系的一套 MySQL 原生访问组件,直接封装 MySQL 客户端协议,不依赖 ODBC 或 BDE 这类中间层,给 Delphi 和 C++Builder 提供 TMyConnection、TMyQuery、TMyTable 这一套组件。适合谁?正在维护 Delphi 存量系统、需要把数据库访问从旧驱动迁出来,或者想研究原生协议封装怎么写的开发者。源文件在手,意味着连接池、数据集缓冲、参数绑定这些平时只能猜的细节,都能翻到实现。

2. 组件架构拆解:从 TMyConnection 到数据集的三层结构

MyDAC 的源码组织方式比较传统,按“连接层—命令层—数据集层”切分,再配一套共享的工具单元。理解这个分层,后面改代码才不会在几百个单元里迷路。

2.1 目录与单元职责划分

拿到源文件包,先别急着编译,花十分钟看单元命名规律。常见做法是按前缀分组:MyAccess、MyConnection这类是连接与认证;MyQuery、MyTable、MyStoredProc是数据集组件;MyCommand、MyParams管 SQL 命令与参数;MyClasses、MyUtils、MyConsts放公共类型、字符串处理和常量;MyDacVcl、MyDacReg负责设计期控件与 IDE 注册。C++Builder 用户还会看到.hpp头文件与.pas成对出现。

单元前缀职责二次开发关注点
MyConnection / MyAccess建立连接、认证、字符集协商连接串解析、超时、SSL 参数
MyCommand / MyParamsSQL 下发、参数绑定预编译语句复用、类型映射
MyQuery / MyTable / MyStoredProc数据集封装、游标管理缓冲模式、Fetch 策略
MyClasses / MyUtils公共基类与工具函数异常体系、内存管理
MyDacVcl / MyDacReg设计期支持一般不改,除非做控件定制

这个划分的好处是职责清晰,坏处是单元间引用比较密,改一个底层类型可能牵动一大片。我一般先只读不写,用 IDE 的“查找引用”把调用链摸清再动手。

2.2 连接建立的关键路径

连接是整个组件的入口,源码里TMyConnection的Connect方法大致走这么几步:解析ConnectionString或分散属性、加载 MySQL 客户端库、调用mysql_real_connect、协商字符集、初始化会话变量。下面这段是示意性还原,帮助理解参数怎么落到实际调用上:

// 示意:连接建立的核心步骤(基于源码结构还原) procedure TMyConnection.DoConnect; begin // 1. 解析连接参数,Host/Port/User/Password/Database 等 ParseConnectionString(FConnectionString); // 2. 加载 libmysql 动态库,找不到就抛异常 InitClientLibrary(FClientLibPath); // 3. 真正建立 socket 连接并完成握手认证 FHandle := mysql_init(nil); if mysql_real_connect(FHandle, PHost, PUser, PPassword, PDatabase, FPort, nil, FClientFlags) = nil then RaiseMyError(mysql_errno(FHandle), mysql_error(FHandle)); // 4. 设置字符集,避免中文乱码 mysql_set_character_set(FHandle, PChar(FCharset)); end;

逻辑说明:ParseConnectionString把host=...;port=...;user=...这种串拆成字段,所以你在设计期填的连接串和运行期赋值的属性最终走同一条路。InitClientLibrary决定用哪个版本的客户端库,这是后面避坑章要重点说的点。mysql_set_character_set必须在连接后立刻调用,晚于第一条查询就可能已经乱码了。参数上,FClientFlags控制是否启用压缩、是否走 Unix socket 等,默认值一般够用,跨网段时再调。

2.3 数据集缓冲与游标管理

TMyQuery继承自自定义的数据集基类,核心是“取一批、缓一批”的策略。源码里能看到FetchRows属性控制每次从服务器拉多少行到本地缓冲,默认值偏小,适合交互式界面;做批量导出时把它调大能明显减少往返。游标分单向和可滚动两种,单向游标内存占用低但不能往回翻,可滚动游标会把结果集缓在客户端。

// 示意:数据集打开与分批取数 procedure TMyQuery.DoOpen; begin PrepareCommand; // 生成并预编译 SQL ExecuteCommand; // 下发查询,拿到结果集句柄 FetchRows := 100; // 每批取 100 行到本地缓冲 FetchFirstBatch; // 首批数据填充,界面可立即显示 end;

这里的关键参数是FetchRows和UniDirectional。前者影响内存与响应速度的平衡,后者决定能不能Prior、Locate往回找。改代码时如果发现大数据量界面卡顿,先看这两个值,而不是急着换组件。

3. 把源文件跑起来:编译、安装与第一个连接

源文件包不是拿来直接双击运行的,它要编进 IDE 的组件面板,或者在运行时动态创建。两条路都走一遍,心里才有底。

3.1 编译前的环境准备

先确认三件事:Delphi 或 C++Builder 的版本与源码包支持的版本匹配;机器上装了对应位数的 MySQL 客户端库(libmysql.dll或libmariadb.dll);源码目录有写权限,因为编译产物和.dcu会落在那里。常见做法是把源码解压到一个短路径下,比如D:\Components\MyDAC,路径里带空格或中文偶尔会让老版本编译器犯迷糊。

编译顺序上,先编运行期包(Runtime Package),再编设计期包(Design Package)。运行期包只含组件逻辑,设计期包额外依赖 IDE 单元,顺序反了会报找不到符号。命令行编译可以用dcc32或msbuild,但新手建议先在 IDE 里打开项目组,逐个 Build,报错信息更直观。

3.2 安装到组件面板

设计期包编译成功后,通过 IDE 的“Install Packages”或“Component → Install Packages”把.bpl加进去。装完在工具面板应该能看到 MyDAC 页签,拖一个TMyConnection到窗体上,双击配置连接参数。如果页签没出现,八成是设计期包没注册成功,检查Register单元有没有被编进去。

// 运行时动态创建连接,不依赖组件面板 var Conn: TMyConnection; begin Conn := TMyConnection.Create(nil); try Conn.Server := '127.0.0.1'; Conn.Port := 3306; Conn.Username := 'app_user'; Conn.Password := '******'; Conn.Database := 'demo'; Conn.LoginPrompt := False; // 不弹登录框,适合服务端 Conn.Connect; // 建立连接 // ... 执行查询 finally Conn.Free; end; end;

逻辑说明:LoginPrompt := False很关键,服务端或后台程序弹登录框会直接卡死。Connect失败会抛EMyError,异常里带 MySQL 错误码,排查时先看错误码再看消息。参数上,Server填 IP 或主机名,Port默认 3306,Database可以先不填,连上后再ChangeDatabase。

3.3 第一个查询与结果读取

连接通了,下一步验证取数。用TMyQuery执行一条带参数的查询,重点看参数绑定和字段读取是否正常。

var Q: TMyQuery; begin Q := TMyQuery.Create(nil); try Q.Connection := Conn; Q.SQL.Text := 'SELECT id, name FROM users WHERE status = :status'; Q.ParamByName('status').AsInteger := 1; // 命名参数绑定 Q.Open; while not Q.Eof do begin Writeln(Q.FieldByName('id').AsInteger, ':', Q.FieldByName('name').AsString); Q.Next; end; finally Q.Free; end; end;

ParamByName走的是预编译路径,参数值不会拼进 SQL 文本,天然避开注入。字段读取用FieldByName每次查找有开销,循环里可以先FieldByName缓存到局部变量。如果Open报“command out of sync”,通常是同一个连接上还有未关闭的结果集,检查有没有漏掉Close。

4. 核心源码精读:参数绑定与错误处理怎么落地

跑通只是第一步,真正要改代码,得看懂两个最容易出问题的地方:参数怎么传到服务器,错误怎么被包装成异常。

4.1 参数绑定的内部流程

TMyQuery的Prepare会把带:name的 SQL 交给服务器预编译,服务器返回语句句柄和参数个数;ParamByName找到对应的TMyParam,赋值时按类型写进绑定缓冲区;Execute时把缓冲区一次性发给服务器。源码里参数类型映射是个大表,整数、浮点、字符串、日期、BLOB 各有分支。

// 示意:参数赋值与绑定缓冲写入 procedure TMyParam.SetAsInteger(const Value: Integer); begin FDataType := ftInteger; FBuffer.AsInteger := Value; // 写入本地绑定缓冲 FIsNull := False; end; procedure TMyCommand.BindParams; var I: Integer; begin for I := 0 to Params.Count - 1 do with Params[I] do if not FIsNull then mysql_stmt_bind_param(FStmt, FBindArray); // 绑定到语句句柄 end;

逻辑说明:FBuffer是本地暂存,mysql_stmt_bind_param才真正把地址告诉客户端库。注意FIsNull标志,赋Null时不能写缓冲区,否则会把脏数据发出去。类型不匹配时源码里会尝试隐式转换,但日期和字符串之间的转换在不同 MySQL 版本行为不一致,血泪经验是显式指定AsDateTime或AsString,别指望自动转。

4.2 错误码到异常的映射

MyDAC 把 MySQL 的错误码包装成EMyError,异常对象里保留ErrorCode和原生消息。源码里有一张错误码到消息模板的映射表,部分错误还会触发自动重连或事务回滚。

// 示意:错误处理与异常抛出 procedure TMyConnection.CheckError(AResult: Integer); var ErrNo: Integer; ErrMsg: string; begin if AResult = 0 then Exit; ErrNo := mysql_errno(FHandle); ErrMsg := string(mysql_error(FHandle)); case ErrNo of 2006, 2013: // 服务器断开 / 连接丢失 begin FConnected := False; raise EMyError.CreateFmt('连接已断开(%d): %s', [ErrNo, ErrMsg]); end; else raise EMyError.CreateFmt('MySQL错误(%d): %s', [ErrNo, ErrMsg]); end; end;

逻辑说明:2006 和 2013 是连接类错误,源码里会把FConnected置 False,避免后续操作继续用坏句柄。业务代码捕获EMyError后,先判断ErrorCode再决定重试还是提示用户。参数上,AResult是客户端库返回值,非 0 即失败,但有些警告也返回非 0,需要结合mysql_errno区分。

4.3 事务与连接池的配合

源码里事务是连接级状态,StartTransaction发BEGIN,Commit发COMMIT,Rollback发ROLLBACK。连接池则在TMyConnection之上做了一层,池里的连接归还时如果还有未提交事务,常见做法是自动回滚,避免脏状态被下一个使用者拿到。

// 示意:事务包裹与异常回滚 Conn.StartTransaction; try Q1.ExecSQL; Q2.ExecSQL; Conn.Commit; except on E: Exception do begin Conn.Rollback; raise; // 回滚后继续上抛,让上层知道失败 end; end;

这里容易翻车的是嵌套事务:MySQL 不支持真正的嵌套,源码里用保存点模拟,StartTransaction第二次调用会建保存点。改代码时如果看到事务没生效,先确认是不是在池化连接上重复开了事务。

5. 避坑与排查:五个真实会遇到的坑

5.1 客户端库位数不匹配

现象:编译通过,一Connect就报“无法加载客户端库”或直接访问违例。原因:Delphi 编的是 32 位,机器上只有 64 位libmysql.dll,或者反过来。解决:确认目标平台位数,把对应位数的库放到可执行文件同目录或系统 PATH 里,源码里InitClientLibrary的搜索路径也可以显式指定。

5.2 字符集设置晚于首次查询

现象:英文正常,中文全是问号或乱码。原因:连接建立后没立刻设字符集,第一条查询已经按默认字符集编码了。解决:在AfterConnect事件或DoConnect末尾调用mysql_set_character_set,统一用utf8mb4,别用utf8,后者存不了四字节字符。

5.3 结果集未关闭导致命令不同步

现象:同一连接上第二条查询报“Commands out of sync”。原因:上一条TMyQuery还开着,游标没释放。解决:用完即Close,或者用TMyQuery的FetchAll一次性取完再关;连接池场景下归还前强制检查活动数据集。

5.4 参数类型隐式转换踩空

现象:日期条件查不到数据,或者数字被当字符串比较导致索引失效。原因:ParamByName没指定类型,源码按默认ftString处理。解决:显式AsDateTime、AsInteger,并在 SQL 里保证列类型与参数类型一致,必要时用CAST。

5.5 连接池归还脏事务

现象:偶发数据不一致,重试又正常。原因:某条路径异常退出,事务没提交也没回滚,连接带着未提交状态回到池里。解决:在BeforeDisconnect或池的归还钩子里检查InTransaction,为真就Rollback;业务层用try...except保证成对。

6. 二次开发:把 MyDAC 包进自己的数据访问层

直接在每个窗体里拖TMyConnection是维护灾难,我一般会在 MyDAC 之上再包一层薄薄的数据访问单元,把连接管理、参数化查询、异常转换收口。改造思路分三步:先定义接口,再实现连接工厂,最后加一个查询辅助函数。

// 示意:轻量数据访问门面 type IDataGateway = interface function Query(const ASQL: string; const AParams: array of Variant): TDataSet; procedure Exec(const ASQL: string; const AParams: array of Variant); end; function TMyGateway.Query(const ASQL: string; const AParams: array of Variant): TDataSet; var Q: TMyQuery; I: Integer; begin Q := TMyQuery.Create(nil); Q.Connection := FPool.Acquire; // 从池里取连接 Q.SQL.Text := ASQL; for I := 0 to High(AParams) do Q.Params[I].Value := AParams[I]; // 按位置绑定,调用方少写参数名 Q.Open; Result := Q; // 调用方负责关闭,或改成返回内存快照 end;

逻辑说明:FPool.Acquire屏蔽了连接创建细节,Params[I].Value按位置绑定减少样板代码,但要求 SQL 里参数顺序稳定。返回TDataSet让调用方自己关,适合短生命周期;如果怕泄漏,可以改成返回TClientDataSet快照,代价是多一次内存拷贝。验证方法很直接:写一个并发测试,开 20 个线程各跑 100 次查询,看连接池是否稳定、有没有句柄泄漏。

改造时还有两个技巧。一是把EMyError统一转成业务异常,别让 MySQL 错误码漏到界面层;二是给FetchRows和UniDirectional做成配置项,报表导出和界面查询用不同预设。至于源码本身要不要改,我的习惯是能不碰就不碰,优先在包装层解决,实在要动底层,先写一组回归用例把连接、查询、事务、异常四条路径覆盖住。从那以后我每次升级组件版本,都强制先跑一遍这组用例再合并,省了不少后悔药。希望帮到你。

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