Absolute Database v7.93源码编译与Delphi避坑
简介Absolute Database v7.93 源代码包面向 Delphi 4 至 11含 Alexandria开发者适合需要研读数据库引擎实现、进行二次开发或做企业级定制的中高级程序员。压缩包共 756 个文件、约 18.9MB以 pas 单元与 hpp 头文件、dcu 编译单元、dpk 与 bdsproj 包工程、sql 脚本、dfm 窗体及 res 资源为主另含 c/cpp 与 asm 底层代码、chm/hlp 帮助文档和 bpl 运行库完整覆盖引擎、组件库与安装包三部分已有 446 人学习下载。读者可据此获得源码级参考事务处理、内存与缓存策略、用户权限与数据加密、备份恢复、多用户并发控制等模块均以 Delphi 源码形式开放便于研究实现细节并按业务需求裁剪面向 DBF、Paradox、Access、SQL Server 的迁移接口与 VCL 组件设计时支持也可用于跨平台部署和旧项目升级。1. 单文件数据库的源码包到底值不值得拆先看它能替你省掉什么某公司的老生产管理系统还在 Delphi 7 上跑数据全塞在 BDE 的别名里客户机换成 64 位系统之后连别名都建不起来车间一次意外断电就丢半张工单表——这是嵌入式数据库每次被重新翻出来的典型场面。这份 Absolute_Database_v7.93_sources_for_D4-11 属于同一类东西把数据库引擎整个做进进程内不装服务端、不配 BDE、一个库就是一个文件而且给的是源码引擎层想改就能改。它面向的是还在维护 Delphi 4 到 Delphi 11 老工程、又不想把架构推倒重来的人。下面按「能不能编、编完怎么用、用起来哪里疼」的顺序拆。2. 从压缩包到可编译工程v7.93 源码在 D4-11 上的落地路径2.1 先看目录再动手源码包的四个组成部分拿到压缩包第一件事不是解到 IDE 安装目录里那个位置一旦被覆盖回滚只能重装。我一般解到D:\SDK\ADB793\这种独立路径工程里通过搜索路径引用出问题直接删目录不留痕迹。解压后的结构各家打包习惯不同但按职责基本能分成四块对着下表核对一遍再动手目录/文件职责编译目标部署时是否需要运行期源码目录若干 .pas引擎、页管理、SQL 解析、索引运行期包 / 直接编进 exe编译期需要运行期不需要设计期注册单元含 Register 过程往 IDE 组件面板注册组件设计期包dcl 前缀不需要只装在开发机帮助文档与 .inc 条件编译头属性说明、版本分支开关无不需要Demo 与示例库建库、查询、事务的最小样例独立工程不需要源码目录里最容易被忽略的是条件编译。为了同时兼容 D4 到 D11单元里会有大量{$IFDEF VER140}、{$IFDEF VER200}这类版本判断——Delphi 4 到 11 跨了十几个编译器版本字符串从AnsiString切到UnicodeString的分界点在 Delphi 2009VER200这套条件编译基本是围着这个分界点写的。改源码之前先确认自己落在分界点哪一侧否则一个Char等于 1 字节还是 2 字节的假设就能让 BLOB 字段读出来全是乱码。还有一点sources包通常不带预编译 DCU意味着第一次编译是全量编译慢是正常的。如果你拿到的是带 DCU 的版本先确认 DCU 的编译版本和你本地编译器一致混用会报「Unit was compiled with a different version of…」这个错看着像源码问题实际是二进制不匹配。提示解压后先对整个目录做一次只读备份改源码前留一份原始副本。这套代码里条件编译分支多改错一个IFDEF后面很难定位。2.2 用 msbuild 命令行把运行期包编出来IDE 里点 Install 最省事但只要涉及多平台Win32/Win64和多版本D7 与 D11 并存命令行更可控。Delphi 的包工程.dpk可以直接交给 msbuild先加载环境变量:: 加载 Delphi 编译环境变量路径换成你本机实际安装位置 call Delphi安装目录\bin\rsvars.bat :: 编译运行期包Win32 Release包文件名以你包内实际为准这里用 ADBRuntime.dpk 代指 msbuild ADBRuntime.dpk ^ /t:Build ^ /p:ConfigRelease ^ /p:PlatformWin32 ^ /p:DCC_UnitSearchPath..\src;..\src\common ^ /p:DCC_DefineADB_DEBUG_OFF ^ /v:minimal/t:Build是全量重建和/t:Clean;Build的区别在于前者不清理中间产物源码改动大时建议先 Clean。/p:Platform必须显式给缺省会落到 Win32 但部分老工程会误判成 Win64编出来的 DCU 与后续链接的 BPL 位数不一致症状是链接期报一堆「Unsatisfied forward or external declaration」。DCC_UnitSearchPath里一定要把源码目录和它的子目录都列上这套源码的内部单元引用有时候是跨子目录的IDE 里加的搜索路径不会自动传给 msbuild。DCC_Define用来关掉调试分支。很多引擎源码里带{$IFDEF DEBUG}的断言和日志写盘Release 下如果还留着跑批量插入时磁盘 IO 会被日志吃掉一大截性能差距能有几倍。编完看输出目录应该有.bpl、.dcp和一堆.dcu.dcp是给别的包引用用的.dcu是给普通工程用的两者都要留着。Win64 再走一遍同样的命令只把Platform换成Win64。这里有个血泪经验这套引擎的指针运算在 32 位和 64 位下行为不同源码里如果有把Integer当指针用的地方32 位能跑、64 位直接访问违例。真要在 Win64 上跑先把源码里所有Integer(...)强转指针的位置过一遍换成NativeInt。2.3 设计期包注册让组件出现在面板上运行期包编通了接着编设计期包。设计期包本身不含逻辑只负责在 IDE 启动时调用注册过程。注册单元的核心就这么几行unit ADBReg; interface procedure Register; implementation uses Classes, ADBUnit; // 运行期主单元名字以包内实际为准 procedure Register; begin // 第一个参数是组件类数组第二个是 IDE 组件面板上的页签名 RegisterComponents(ADB, [TADDatabase, TADSession, TADTransaction, TADTable, TADQuery]); end; end.RegisterComponents的第二个参数决定组件挂在面板哪一页起个自己好找的名字就行重复了 IDE 会把两组并到同一页。设计期包的requires里必须包含rtl、vcl如果组件有可视祖先以及刚编好的运行期包缺一个都会在安装时报「Cannot load package」。安装走 IDE 的 Component → Install Packages → Add选中编出来的dcl*.bpl。装完如果面板上是空的八成是设计期包引用了另一个路径下的同名运行期包两个 BPL 版本对不上把 IDE 的 Library Path 里的旧路径删掉只留当前这份再重启 IDE。2.4 编完先跑冒烟测试别急着往老工程里塞包能装上不代表引擎能用。我习惯先写一个控制台小程序把建库、插入、查询、事务回滚四条路径走一遍通过了再往生产工程里引program ADBSmoke; {$APPTYPE CONSOLE} uses System.SysUtils, ADBUnit; // 代指实际单元名以包内为准 var Db: TADDatabase; Qry: TADQuery; begin Db : TADDatabase.Create(nil); try Db.DatabaseName : D:\temp\smoke.adb; Db.LoginPrompt : False; // 控制台程序没有登录框必须关掉 Db.Open; Qry : TADQuery.Create(nil); try Qry.Database : Db; Qry.SQL.Text : CREATE TABLE T1 (ID INTEGER, NAME VARCHAR(50)); Qry.ExecSQL; // 事务内插入验证提交与回滚两条路径 Db.StartTransaction; Qry.SQL.Text : INSERT INTO T1 (ID, NAME) VALUES (:ID, :NAME); Qry.ParamByName(ID).AsInteger : 1; Qry.ParamByName(NAME).AsString : smoke; Qry.ExecSQL; Db.Commit; Qry.SQL.Text : SELECT COUNT(*) FROM T1; Qry.Open; Writeln(rows, Qry.Fields[0].AsInteger); // 期望 1 Qry.Close; finally Qry.Free; end; finally Db.Close; Db.Free; end; end.这段代码的验证点有三个LoginPrompt : False能不能在无界面环境打开库、StartTransaction/Commit是否真的落盘、参数化查询的:ID占位符是否被正确识别。三个都过说明引擎主体是好的剩下的都是配置问题。跑完去D:\temp\下看除了smoke.adb还应该有同名的临时日志文件提交后一般会被回收。如果日志文件一直不消失说明事务没正常提交回到try...finally里检查是不是中途抛异常跳过了 Commit。第一次跑建议开着 Process Monitor 看文件句柄能提前发现引擎是不是在偷偷往系统临时目录写东西——有些版本会把排序临时文件放到%TEMP%在磁盘配额紧张的环境里会出问题。3. 存储引擎层页式文件、字段编码与索引维护的实现细节3.1 一个 .adb 文件里到底装了什么单文件数据库听起来像个黑匣子拆开看其实是固定页大小的分页文件。文件头第一页存元信息页大小、版本号、加密标志、页分配位图起点后面是数据页、索引页、BLOB 页和系统表页混排。页大小在建库那一刻定下来之后不能改——这一点和某些服务端数据库可以动态调整不同是选型时必须先想清楚的。页大小直接影响两件事单条记录的上限和一次 IO 读多少。页越小BLOB 越容易被拆成链式页读一个大字段要多次寻道页越大小记录的浪费越明显。我一般的取法是表里以短记录为主、并发读多用较小页1KB2KB有大量文本或二进制字段用 4KB8KB。改页大小只能重建库所以建库脚本里这个参数要写死并加注释别等到上线三个月才发现不合适。页分配位图是判断文件健康度的入口。源码里维护位图的单元通常有一个「空闲页计数」正常关闭时这个值应该和文件实际大小对得上。如果进程被强杀位图可能停在中间态下次打开时引擎会走一遍修复流程速度明显变慢。这也是为什么这套引擎不适合放在会被突然断电的工控机上裸跑——要么配 UPS要么把提交频率调高用性能换一致性。3.2 字段类型到物理字节的映射选字段类型不能只看语义要看它怎么落盘。定长类型在页内是原地更新变长类型改长度可能触发记录搬迁进而影响索引指针。下面这张表是我在迁移老表结构时对照用的字段类型定长/变长索引友好度迁移时要注意的点短整型 / 整型定长高比较是数值比较注意 64 位平台下的宽度别用整型存指针浮点 / 定点定长高但等值比较不可靠金额类务必用定点或整数分浮点做等值查询会漏行定长字符串定长高长度按字符还是字节算Unicode 下差一倍变长字符串变长中超长值不进索引长度阈值以上会被排除在索引外查询要走全表日期时间定长高老库可能存的是本地时间跨时区要统一BLOB / 大文本变长链式页不可索引只存引用正文单独放避免主表页被撑爆变长字符串那一条是踩过坑的。很多引擎对超长值做「前缀索引」超出阈值的部分不参与索引比较于是WHERE NAME 很长很长的一串会走索引但查不到只能扫全表。迁移时如果把原来VARCHAR(255)改成VARCHAR(4000)索引行为可能悄悄变了测试用例里必须有一条超长值的等值查询。日期时间字段的坑更隐蔽。老系统里常见「存字符串、应用层格式化」的做法换到有原生日期类型的引擎之后排序和范围查询才对得上。迁移脚本里要做一次显式转换别指望引擎自动识别2024-01-01这种格式不同区域设置下解析结果可能差一天。3.3 索引维护B 树分裂与回滚日志索引在这套引擎里是独立的 B 树主键和普通索引各占一棵。插入导致页满时触发分裂分裂本身是原子的但分裂过程中锁的粒度比普通插入大所以高并发写入的表上自增主键比随机主键的吞吐明显更好——随机插入会到处触发分裂页缓存命中率直线下降。这一点在批量导入时体现得最明显。同样十万条记录按主键递增顺序插入可能几十秒打乱顺序插入能慢好几倍。我的做法是导入前先按主键排序或者干脆导入时先删掉二级索引、导完再重建代价是重建期间表不可写。事务回滚靠的是同目录下的日志文件。提交前所有修改先写日志提交时刷盘并标记回滚时按日志反向撤销。这里有个容易误判的现象事务开着的时候.adb文件大小可能一直不涨因为数据还在日志里提交瞬间文件才膨胀。有人看到文件不涨就以为没写进去重复提交结果插了两遍。判断有没有写进去看日志文件的修改时间比看主文件大小靠谱。崩溃恢复的边界要说清楚引擎能保证「已提交的不丢」但对「未提交的」只做到尽量回滚极端情况下日志文件本身损坏会退化成修复模式可能丢最后一批未提交数据。所以关键业务里别把「事务」当成「备份」定期把.adb文件冷拷贝一份出去仍然是最便宜的安全网。3.4 TADTable 还是 TADQuery两套访问方式的取舍这套组件同时提供表式访问和 SQL 访问两套接口。表式接口对应 TADTable走的是记录缓冲适合逐条编辑、带界面的数据维护SQL 接口对应 TADQuery走解析执行适合批量操作和复杂条件。混用要小心。同一张表上一个 TADTable 开着记录缓冲、另一个 TADQuery 在改同一行两边的缓存不会自动同步界面上看到的还是旧值。老工程里这种「表格控件绑一个、代码里再开一个查询」的写法很常见症状是改完刷新才看得到。解决办法是用同一个数据集控件或者改完主动调一次刷新。批量写入优先用 SQL 接口加参数化表式接口逐条 Append/Post 的开销在万级数据上非常明显。我做过对比同样一万条插入表式接口在事务里跑和参数化 SQL 在事务里跑后者通常快一个量级差就差在每次 Post 的缓冲管理和索引维护上。4. SQL 执行链路解析、参数绑定与并发边界的实测记录4.1 一条 SELECT 从文本到结果集经过了什么这套引擎的 SQL 支持是子集式的不是完整标准。一条语句进来大致走四步词法语法分析生成语法树、语义检查表存不存在、字段类型对不对、计划选择走哪个索引、要不要排序、执行取数。计划选择这块比服务端数据库朴素得多基本是规则式的WHERE 里有等值条件且字段有索引就走索引有范围条件走索引区间扫描LIKE %x%这种前导通配直接全表扫。没有基于统计信息的代价模型所以「同一个查询昨天快今天慢」在这套引擎上很少见好处是可预测坏处是复杂查询得自己 rewrite。理解这一点很关键不要指望它像服务端数据库那样自动帮你选最优计划。写法上要主动配合把选择性最高的条件放前面只是习惯问题真正影响的是「有没有可用索引」和「能不能避免排序」。ORDER BY如果没有匹配索引会走内存排序数据量大时溢出到临时文件这时候性能断崖式下跌。执行层有记录缓冲Open之后不是一次性把结果全读进来而是按需取。这意味着Open很快但遍历慢如果只取第一行就Close开销很小反过来如果用一个查询遍历十万行再逐行处理总耗时里取数占大头。批量处理时用Fetch循环或者干脆用 SQL 做聚合比拉全量到应用层快得多。4.2 参数化查询与 LIKE 的真实代价参数化不只是防注入在这套引擎上它还是性能问题。SQL 文本不变时解析结果可以复用每次拼接字符串解析都要重来一遍。批量操作里这个差距被放大得很明显// 反例循环里拼 SQL每次都要重新解析 for I : 0 to List.Count - 1 do begin Qry.SQL.Text : Format(UPDATE T1 SET V%d WHERE ID%d, [List[I].Value, List[I].ID]); Qry.ExecSQL; end; // 正例SQL 只设一次参数在循环里改 Qry.SQL.Text : UPDATE T1 SET V:V WHERE ID:ID; Qry.Prepare; // 显式预编译部分版本不调也可调了更稳 Qry.Params.ParamByName(V).DataType : ftInteger; for I : 0 to List.Count - 1 do begin Qry.ParamByName(V).AsInteger : List[I].Value; Qry.ParamByName(ID).AsInteger : List[I].ID; Qry.ExecSQL; end;Prepare的作用是把解析和计划选择提前做掉循环里只做参数绑定和执行。参数必须显式指定DataType否则第一次绑定的类型会被沿用后面传不同类型的值会报类型转换错或者更糟——被静默截断。AsInteger这类属性赋值也依赖DataType已确定顺序别搞反。前导通配的LIKE是另一个高频翻车点。LIKE abc%能走索引区间扫描LIKE %abc和LIKE %abc%一律全表扫。业务上如果真有「包含」查询的需求做法是建一张倒排的辅助表把关键词拆出来存一行或者把数据同步到专门做检索的组件里别指望在这套引擎上硬扛。十万行以上的表一个前导通配 LIKE 就能让界面卡住十几秒用户以为死机。还有一个参数化的边界IN列表不能参数化。WHERE ID IN (:A, :B)这种写法在部分版本里只认第一个参数。要做动态IN只能拼字符串这时候必须自己保证列表元素是整数或做过转义别把用户输入直接拼进去。4.3 多线程与多进程访问的边界这套引擎的进程内并发模型是「一个连接一个会话」。TADSession是隔离单位同一个 Session 下的组件共享事务上下文跨 Session 就是两个独立连接。多线程访问时最容易犯的错是所有线程共用一个TADQuery症状是偶发的「数据集未打开」或者数据串行非常难复现。正确做法是每个线程自己 Create 一套 Session 和 Query用完释放。连接本身是线程内使用的不要跨线程传递。引擎内部对文件有页级锁多线程写入同一张表时会互相等待所以写入线程数量不宜多两三个就够再多收益递减还增加死锁概率。多进程是另一回事。同一台机器上两个进程打开同一个.adb文件引擎靠文件锁协调能跑但性能差而且进程被强杀时锁可能残留表现为「另一个程序正在使用此文件」。我的处理方式是写一个.lock标记文件启动时检查异常退出后人工确认再删比引擎自己的锁更可控。最要命的是把.adb放在网络共享目录里。网络文件系统的锁语义和本地不同两个客户端同时写会出各种奇怪的数据损坏而且这种损坏往往是延迟暴露的——今天写完看着没事明天打开发现索引对不上。这条是硬红线单文件数据库只能放本地盘或者块设备别放共享盘。5. 避坑与排查编译、加密、并发与升级的四类翻车现场5.1 编译期报找不到单元或者报「compiled with a different version」现象命令行编译报F1026 File not found: System.SysUtils.dcu或者在 IDE 里编过了、换 msbuild 就报Unit ADBUnit was compiled with a different version of System.Classes。原因前者是搜索路径没带全rsvars.bat只设了环境变量工程里引用的第三方源码目录还得靠DCC_UnitSearchPath显式给后者是 DCU 版本串味——你的搜索路径里同时存在两套不同编译器版本编出来的 DCU或者当前目录下的 DCU 比源码旧、msbuild 判断依赖时没重编。解决第一步清空输出目录重编很多时候就是残留 DCU 的问题第二步在 msbuild 命令里加-verbosity:detailed看它实际用了哪个路径的单元路径列表一眼就能看出多出来的那条第三步把DCC_UnitSearchPath里所有绝对路径检查一遍别用相对路径混搭。如果是从老工程迁移过来检查工程文件里有没有硬编码的旧路径那东西不会因为改环境变量而失效。5.2 库文件换台机器就打不开提示密码错误现象开发机上一切正常拷到客户机上打开报密码错误或者「不是有效的数据库文件」密码确定没输错。原因三个可能。第一加密算法依赖的运行时库在客户机上缺引擎走了降级路径第二密码里带非 ASCII 字符开发机和客户机的默认代码页不同字符串编码在传给引擎之前就变了第三客户机上装的是另一套同名组件注册表或系统路径里的 BPL 优先被加载。解决密码策略统一用 ASCII 可见字符别用中文和特殊符号这一条能省掉八成的跨机器问题。部署包里的 BPL 和 DCU 版本要锁死安装时检查目标机是否已存在同名 BPL存在就先备份。加密库如果带独立的 DLL把它和 exe 放同一目录别放系统目录——放系统目录之后升级会互相覆盖症状是「以前能开现在开不了」查起来极费时间。5.3 多线程下偶发数据错乱或「session busy」现象单线程测试全部通过上了多线程之后隔几个小时出现一次数据不对或者抛出会话忙的异常重启进程就恢复。原因共享了同一个数据集组件或者共享了同一个事务对象。事务上下文绑定在 Session 上两个线程在一个 Session 里各自 StartTransaction后面 Commit 的那个会把另一个的未提交数据一起提交掉回滚同理直接造成数据不一致。解决线程内自建 Session用完释放绝不复用。给每个线程的 Session 起个可辨识的名字比如带线程 ID出问题时日志里能对上。事务里做的事尽量短别在事务中间去调外部接口或者弹框等用户输入。写入线程数控制在两三个以内实测再往上加收益很小死锁概率反而上升。日志里出现「会话忙」的时候先看有没有长事务没提交八成是某个分支抛异常跳过了 Commit 或 Rollback。5.4 用新版本组件打开老库之后表结构看着不对现象从旧版本升级上来用新组件打开原来的.adb能连上但某些字段显示为空、长度变了或者索引失效导致查询变慢。原因字段类型的物理宽度在新版本里调整过典型是字符串长度和日期精度老库的元信息按老规则解析另外索引的页格式如果有变化旧索引要么被标记失效要么读出来顺序不对。解决升级前先做一次「导出—重建—导入」别直接在老库上开新组件写数据。导出用老版本的组件导成文本或中间表重建库时把页大小、字段类型、索引一次性定好导入完逐表对比行数和关键字段校验和。升级脚本要可重复执行出问题时能回到上一步。这一步看着笨但比事后从备份里捞数据便宜太多。6. 加密与交付前自检把 .adb 交出去之前的最后一道关带加密的单文件数据库有个特性文件本身就是全部。谁能拿到这个文件加上密码谁就拿到全部数据。所以交付前的检查不能只看程序能不能跑得看这份文件被拷走之后会发生什么。我一般按这张表过一遍检查项判断标准不通过的后果加密是否真的生效用十六进制工具看文件头明文标识应该不可见加密属性设了但没生效等于裸奔密钥是否硬编码在 exe 里字符串搜索能搜出密码片段就是没做反编译即可拿到全库是否有明文临时文件检查%TEMP%和程序目录排序、导出过程落明文版本号与库文件是否匹配旧程序打开新库应明确报错静默写坏数据干净机器验证没装过开发环境的机器上跑一次全流程缺运行库现场打不开密钥不要写死在代码里是常识但落地时很多人图省事还是硬编码。折中做法是把密钥拆成几段一段在配置文件、一段由机器特征算出来缺任何一段都打不开成本不高但能挡住顺手拷贝。真要更严把库文件本身再做一层应用层字段级加密敏感字段在入库前就加密引擎里存的是密文——代价是这些字段不能索引也不能做范围查询取舍要提前想清楚。干净机器验证这一步千万别省。我见过太多次「开发机好好的客户现场打不开」原因全是运行库缺失或者 BPL 版本不对。准备一台只装操作系统的虚拟机把安装包从头装一遍建库、写数据、导出、备份恢复四条路径全走通才算交付合格。从那以后我每次交付带.adb的安装包都强制走一遍上面那张表尤其是拿一台干净机器从头跑一次——这一步花二十分钟能省掉现场两天。希望帮到你。本文还有配套的精品资源点击获取