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

简介MySQL Data Access ComponentsMyDACv5.10 源文件面向使用 Delphi 7 至 2007 及 CBuilder 的数据库开发者用于在不依赖第三方库或中间件的前提下直接连接 MySQL 服务器完成数据读取、写入、更新与删除等操作。组件集成了事务处理、批处理、存储过程、异步操作与错误处理机制兼顾高性能与低资源占用适合构建数据交互频繁的桌面应用。压缩包共 1403 个文件约 1.39MB以 pas 源文件、dpk 与 dpr 工程文件、dfm 窗体文件、res 资源文件及 cfg 配置为主另含帮助文档、许可协议与编译运行说明便于按模块查阅与二次定制。已有 168 人学习下载。源文件形式让开发者可深入组件内部实现按项目需求调整连接逻辑与功能扩展是 Delphi/CBuilder 环境下 MySQL 开发的一份实用参考。1. 一套 Delphi 老项目的数据库层为什么值得单独拆开看接手一个跑了七八年的 Delphi 客户端时最怕的不是界面乱而是数据访问层像黑匣子连接串散落在十几个窗体里SQL 拼接靠字符串加法换一台机器就报驱动版本不对。这时候翻到 MyDAC v5.10 的源文件会有种“终于能看见底层”的感觉。它是 Devart 系的一套 MySQL 原生访问组件直接封装 MySQL 客户端协议不依赖 ODBC 或 BDE 这类中间层给 Delphi 和 CBuilder 提供 TMyConnection、TMyQuery、TMyTable 这一套组件。适合谁正在维护 Delphi 存量系统、需要把数据库访问从旧驱动迁出来或者想研究原生协议封装怎么写的开发者。源文件在手意味着连接池、数据集缓冲、参数绑定这些平时只能猜的细节都能翻到实现。2. 组件架构拆解从 TMyConnection 到数据集的三层结构MyDAC 的源码组织方式比较传统按“连接层—命令层—数据集层”切分再配一套共享的工具单元。理解这个分层后面改代码才不会在几百个单元里迷路。2.1 目录与单元职责划分拿到源文件包先别急着编译花十分钟看单元命名规律。常见做法是按前缀分组MyAccess、MyConnection这类是连接与认证MyQuery、MyTable、MyStoredProc是数据集组件MyCommand、MyParams管 SQL 命令与参数MyClasses、MyUtils、MyConsts放公共类型、字符串处理和常量MyDacVcl、MyDacReg负责设计期控件与 IDE 注册。CBuilder 用户还会看到.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 或 CBuilder 的版本与源码包支持的版本匹配机器上装了对应位数的 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默认 3306Database可以先不填连上后再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发BEGINCommit发COMMITRollback发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做成配置项报表导出和界面查询用不同预设。至于源码本身要不要改我的习惯是能不碰就不碰优先在包装层解决实在要动底层先写一组回归用例把连接、查询、事务、异常四条路径覆盖住。从那以后我每次升级组件版本都强制先跑一遍这组用例再合并省了不少后悔药。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。