简介:Absolute Database v7.93 的 Delphi 源代码包,面向需要跨版本使用 Delphi 4~11 的数据库开发人员,以及桌面、嵌入式和企业级应用开发者。该版本兼容最新的 Delphi 11 Alexandria,核心解决传统数据库引擎在 IDE 集成、事务处理、内存缓存、备份恢复与多用户并发方面的需求,同时开放底层源码,便于深入二次定制。压缩包内含756个文件,共18.9MB,主要覆盖SQL脚本、HPP头文件、PAS单元、DFM窗体、DPK组件包等类型,其中SQL脚本提供建表与示例数据,PAS/DFM/DPK构成组件库与设计期支持,HPP/OBJ/C等则关联底层编译链接,可辅助理解数据库引擎构建与组件封装。已有443人学习下载,适合用于研究 Absolute Database 内部机制、移植旧项目,或基于源码扩展安全加密、权限管理等功能。通过源码可掌握从缓存策略到查询执行的完整脉络,并借助 Delphi 11 的设计时支持快速搭建数据库应用,是 Delphi 生态中兼具教学和工程价值的参考资料。
1. 项目定位:一个编译进 EXE 的嵌入式数据库
接手老 Delphi 系统的人都懂那种无力感:BDE 在新 Windows 上装不上,ADO 连 Access 动不动锁库,客户还要求单机免安装。拆完这套 Absolute_Database_v7.93_sources_for_D4-11,算是把这个问题解透了。它不是一个需要安装服务的数据库服务器,而是编译进你 EXE 的嵌入式数据库组件,从 Delphi 4 到 Delphi 11 的源码都能编译,数据落在单一文件里,读写、事务、加密全套自带。适合还在维护老工程、想甩掉 BDE 的 Delphi 开发者,也适合新项目里要零部署、免安装的桌面端团队。
2. 架构拆解:源码目录、运行机制与选型对比
2.1 源码包目录:Core、Packages 与 Demo 的分工
v7.93 的目录结构非常清爽,没有几百个文件平铺的杂乱感。外层按「源码、工程、示例、文档」四个块拆开,拿到手不用猜。Source 目录是运行时核心,每个 unit 按职责划分:连接管理、表操作、SQL 查询、字段类型定义。Packages 目录是编译入口,按 Delphi 版本分组,从 D4 到 D11 都有对应的 dpk 工程。Demos 里的示例覆盖了普通表单绑定、加密库和网络共享三种用法,建议先跑 SimpleDemo 再拆别的。
Absolute_Database_v7.93_sources_for_D4-11/ ├── Source/ │ ├── ABSDB.pas # 引擎核心:连接、事务、建库 │ ├── ABSTable.pas # 表组件,继承自 TDataSet │ ├── ABSQuery.pas # SQL 查询组件 │ ├── ABSData.pas # 字段类型和数据字典 │ └── ABSUtil.pas # 内部工具函数 ├── Packages/ │ ├── D4-D7/ # 老版本工程 │ ├── D2009-D2010/ # Unicode 分界点 │ ├── DXE-DXE8/ │ └── D10-D11/ # 新版本工程 ├── Demos/ │ ├── SimpleDemo/ # 表单一对一绑定示例 │ ├── EncryptedDemo/ # 加密库示例 │ └── NetworkDemo/ # 多客户端共享数据示例 └── Docs/ └── AbsoluteDB.chm # 帮助文档文件名的 ABS 前缀是这套组件的统一命名,你在代码里看到 TABSxxx 开头的类都来自这里。Source 与 Packages 必须配套:Source 是运行时单元,Packages 只是把它们组织成可安装到 IDE 的包。如果只拷贝 Source 到自己的工程路径、不装 IDE 包,也可以直接用,编译期把 Source 目录加进搜索路径就行;装 Packages 主要是为了在 IDE 组件面板里拖拽组件。
2.2 运行架构:进程内引擎与单文件存储
这套库和 C/S 数据库最大的区别是没有独立服务进程。编译时引擎代码直接并进 EXE,运行时由它管理一个数据文件(默认扩展名 .abs)。数据文件内部自己维护页、B-Tree 索引、事务日志,对外就是一个文件。应用层调用链是:界面代码 → TABSQuery/Table → 引擎解析执行 → 文件 I/O。因为没有网络层和进程通信,单机场景下比 C/S 方案省掉一大截延迟,也少了很多「数据库服务没启动」这类运维事故。
从数据安全角度看,单文件存储是把双刃剑。好处是备份只拷一个文件,坏处是断电时文件损坏风险集中在单点。引擎内部用事务日志做崩溃恢复,但我一直遵循一个习惯:重要数据半小时一次在线备份,或者用系统计划任务定时复制文件,别把「嵌入式」当成「不会坏」。
2.3 选型对比:和 SQLite、BDE 的取舍
| 对比项 | SQLite | BDE + Paradox/dBase | Absolute_Database v7.93 |
|---|---|---|---|
| 部署文件 | 需要 sqlite3.dll | 需要安装 BDE 驱动,兼容性差 | 纯 Delphi 静态链接,无额外 DLL |
| 数据组织 | 单文件 | 多文件(表、索引、备注分离) | 单文件 .abs |
| 事务能力 | ACID,默认需开启 WAL | 弱,锁文件易损坏 | ACID,自带事务日志 |
| 加密 | 需要额外扩展 | 基本没有 | Blowfish,建库时指定密码 |
| 与 Delphi 绑定 | 外部 API 调用,封装成本高 | 控件级,但已废弃 | TDataSet 兼容,可拖拽绑定 |
| 网络并发 | 主写单连接 | 文件锁,多写易坏 | 适合小规模共享读 |
这张表是我做技术选型时留下的对比依据。SQLite 在移动端统治力强,但在老 Delphi 工程里接入要引入外部 DLL,还涉及 API 封装,改造量不小。BDE 是新系统装不上、老系统能用但文件结构脆弱的典型「历史包袱」。这套组件的核心优势其实是最后一行:它直接实现 Delphi 的 TDataSet 接口,DBGrid、DBEdit 这些可视化组件不需要改绑定方式,从 BDE 迁移的改动被压缩到数据访问层。
3. 核心代码实战:建库、读写、事务与加密
3.1 连接与建库:三行代码跑通第一个数据库
嵌入式数据库的起步动作都一样:给引擎一个文件路径,然后打开它。注意这套组件里 DatabaseName 指的不是逻辑别名,而是完整的物理文件路径,这是和 BDE 差异最大的地方,也是新手最容易踩的点。
var DB: TABSDatabase; begin DB := TABSDatabase.Create(nil); try DB.DatabaseName := 'D:\data\demo.abs'; DB.Connected := True; // 文件不存在会自动创建 DB.ExecuteDirect( 'CREATE TABLE IF NOT EXISTS user_info (' + ' id INTEGER PRIMARY KEY, ' + ' name VARCHAR(50), ' + ' age INTEGER, ' + ' created_at TIMESTAMP)' ); finally DB.Free; end; end;逻辑说明:Connected 置 True 时引擎做三件事:检查路径合法性、打开或创建数据文件、初始化事务日志。之后 ExecuteDirect 执行 DDL,这套组件的 SQL 语法是类标准 SQL,支持 CREATE TABLE / INSERT / UPDATE / SELECT,但别指望它和大型数据库的方言完全一致,存储过程和触发器这类高级对象的支持非常有限。
参数说明:DatabaseName 的目录必须事先存在,引擎不会帮你递归创建目录;CREATE TABLE 里的 IF NOT EXISTS 建议默认写上,重复执行初始化代码不会报错。如果建库时指定了加密,Connected 之前没设密码会直接抛异常,后面事务一节我会专门讲。
3.2 两种访问方式:TABSTable 与 TABSQuery
组件同时提供了两种数据访问方式:TABSTable 对标 BDE 的 TTable,适合整表操作、网格浏览;TABSQuery 对标 TQuery,适合跑带条件的 SQL。两者的差异不只是 API,底层执行路径也不同,Table 走的是引擎直接读表,Query 会先生成执行计划再取数。
// 方式一:TABSTable 整表浏览 TABSTable1.DatabaseName := 'D:\data\demo.abs'; TABSTable1.TableName := 'user_info'; TABSTable1.Open; while not TABSTable1.Eof do begin ShowMessage(TABSTable1.FieldByName('name').AsString); TABSTable1.Next; end; // 方式二:TABSQuery 条件查询 TABSQuery1.DatabaseName := 'D:\data\demo.abs'; TABSQuery1.SQL.Text := 'SELECT * FROM user_info WHERE age > :minAge'; TABSQuery1.ParamByName('minAge').AsInteger := 18; TABSQuery1.Open;逻辑说明:Table 打开后数据集和组件绑定在一起,适合改动后直接 Post 回写;Query 只在调用 Open 后获得结果集,写操作通常用 ExecSQL 而不是 Open。ParamByName 是参数化查询的标准姿势,比拼字符串安全得多,能避免 SQL 注入问题。
参数说明:TableName 必须对应库里已存在的表,否则 Open 会报「表不存在」;Query 的参数名以冒号开头声明,赋值时用 ParamByName 并按名字访问,参数顺序无关。两种方式都继承 TDataSet,DataSource 那一套绑定直接可用,DBGrid 显示出来的效果和 BDE 时代没有差别。
3.3 事务与加密:ACID 兜底和数据落盘保护
单文件数据库的常见质疑是「文件型的东西扛不扛得住中断」,答案在事务日志里。StartTransaction 之后的所有写操作先记日志,Commit 才真正落盘,中间任何一步异常都可以 Rollback 回到起点。
// 事务:转账示例 DB.StartTransaction; try DB.ExecuteDirect('UPDATE accounts SET balance = balance - 100 WHERE id = 1'); DB.ExecuteDirect('UPDATE accounts SET balance = balance + 100 WHERE id = 2'); DB.Commit; except DB.Rollback; raise; end;逻辑说明:事务块里两行 UPDATE 要么都成功要么都失败,不存在只扣不加的中间态。Commit 之后数据才对外可见,Rollback 会丢弃本事务所有修改。注意这套组件不支持嵌套事务,外层没提交时再调 StartTransaction 的行为不太直观,我一般用「事务中置标志位」的方式防止重复进入。
加密是另一层保护。文件型数据库最怕别人拿到文件拖库,Blowfish 加密可以在建库时指定密码,之后每次连接都要给对密码才能打开。
DB.EncryptionAlgorithm := abEncryptionBlowfish; DB.EncryptionPassword := 'your-pass-phrase'; DB.Connected := True;参数说明:加密参数必须在 Connected 为 False 时设置,连接建立后改密码需要走专门的「改密」流程,不要直接改属性后重连。密码忘了我只能说很遗憾——这个库的设计理念是密码即钥匙,没有后门,加密后的文件拿不到密码几乎解不开。应用里我会把密码放在配置文件的加密段里,而不是硬编码在源码中。
3.4 BLOB 大字段:把文件和图片塞进数据库
如果业务里有附件、签名图片、报表模板,BLOB 字段就派上用场了。这套组件对 BLOB 的处理方式遵循 Delphi 标准:拿到 TBlobField,然后 LoadFromStream 或 SaveToStream。
var FS: TMemoryStream; begin FS := TMemoryStream.Create; try FS.LoadFromFile('C:\data\scan_001.pdf'); TABSTable1.Append; TABSTable1.FieldByName('doc_no').AsString := 'DOC-001'; TBlobField(TABSTable1.FieldByName('doc_blob')).LoadFromStream(FS); TABSTable1.Post; finally FS.Free; end; end;逻辑说明:Append 创建新记录,Post 提交到当前数据集。TBlobField.LoadFromStream 把流对象整体写入字段,底层引擎会把二进制内容切分存储到文件的数据页里。读取时反向操作:SaveToStream 到 TMemoryStream 再 SaveToFile。
参数说明:BLOB 字段类型在 DDL 里写 BLOB 或 MEMO(文本大对象)。这里有个实用建议:单条 BLOB 数据超过 64MB 时,写入耗时指数级上升,照片、PDF 完全没问题,但视频文件不要往库里塞,存路径更合适。引擎对大字段默认延迟加载,Open 表时不会把所有 BLOB 都读进内存,而是取到才读,这是它能在内存受限环境跑的原因。
4. 编译与迁移:从 D4 到 D11 的跨版本落地
4.1 跨版本编译器差异:源码里的大量条件编译
一份源码同时支持 D4 到 D11,意味着它必须跨越 Delphi 历史上最大的分水岭——2009 年引入的 Unicode string。D4 时代 string 是 AnsiString,按字节处理;D2009 之后 string 是 UnicodeString,按字符处理。字符串长度、字符编码、文件读写全部受影响。源码里大量出现 {$IFDEF UNICODE} 正是为这个准备的。
{$IFDEF UNICODE} FUserName: string; // D2009+ 环境:UnicodeString {$ELSE} FUserName: AnsiString; // D7 及更早:AnsiString {$ENDIF}逻辑说明:条件编译让同一份代码在不同编译器下选取不同声明。除了 string 类型,受影响的地方还包括 Char 大小(1 字节变 2 字节)、Length 函数返回的语义、以及 AnsiString/PChar 的强制转换。你在改这套库时,凡是看到 IFDEF 包裹的代码,都是作者为跨版本付出的妥协,动它们要极其小心。
4.2 编译安装步骤:按目标版本把组件装进 IDE
装包的正确姿势是按「运行时包 → 设计时包」的顺序来。运行时包负责把 Source 单元编译进你的 EXE,设计时包负责在 IDE 里显示组件图标。顺序反了会报「Package 不存在」或者 IDE 找不到 BPL。以一个假设的 D11 环境为例:
- 打开 Packages/D10-D11 目录,里面有 Runtime.dpk 和 Design.dpk;
- 先打开 Runtime.dpk,菜单 Compile,确认编译通过;
- 再打开 Design.dpk,菜单 Install,等 IDE 弹出安装成功提示;
- 新建一个 VCL 工程,在组件面板搜 ABS,能看到 TABSDatabase、TABSTable、TABSQuery 就算成功。
老版本环境(D4~D7)的步骤相同,唯一差别是 dpk 文件格式和 IDE 的安装按钮位置——D7 点 Install 在 Component 菜单下,新版本在 Package 窗口里。命令行编译也有方案,用 dcc32 加 -JP 参数编译运行时包,但 IDE 里点按钮更省事。装好之后务必关掉 IDE 重启一次,让 BPL 缓存刷新,否则首次拖组件可能报找不到类。
4.3 接入旧工程:替换 BDE 的最小改动清单
如果你手里是一个正在用 BDE 的老工程,迁移的改动范围可以参考下表。核心原则是:界面层(DBGrid、DBEdit、DataSource)不用动,只换数据访问组件。
| 原 BDE 组件 | 替换为 | 注意点 |
|---|---|---|
| TDatabase | TABSDatabase | 连接参数替代别名 |
| TTable | TABSTable | TableName 从别名表名改为物理表名 |
| TQuery | TABSQuery | SQL 属性兼容,方言有小差异 |
| TDataSource | 不变 | TDataSet 接口一致 |
| TBatchMove | 无对应 | 手工循环拷贝 |
替换后第一步是查 SQL。BDE 的 SQL 支持千奇百怪,尤其是 dBase 的表名带路径、Paradox 的字段名带引号,这些在嵌入式的 SQL 引擎里都不适用。我建议用 Demo 先建一个临时库,把老工程里每条 SQL 搬进去跑一遍,重点看 JOIN、子查询、日期函数三种写法。日期函数差异最坑:BDE 里用YEAR(created_at),这套引擎可能不认识,得改成EXTRACT(YEAR FROM created_at)这类标准写法。
4.4 发布部署:输出物只有一个 EXE
嵌入式最大的红利在发布阶段。传统 BDE 程序发布要带一堆驱动 DLL、BDE 配置、别名注册脚本,新版 Windows 还时不时报兼容性错误。这套库把数据库引擎静态编译进 EXE,发布物就是程序本体加上数据文件。
发布目录/ ├── MyApp.exe # 主程序,引擎已编译进来 ├── data/ │ └── app.abs # 数据库文件,首次运行自动创建 └── config.ini # 连接参数与加密配置逻辑说明:三个文件就是完整发布物。没有额外 runtime、没有注册表项、没有 ODBC 配置。我通常把 DatabaseName 和加密密码放在 config.ini 里,加密状态下首次连接密码由程序读取后传入,这样更换电脑时只需拷贝目录、改配置文件路径即可完成部署。升级数据库结构时把 ALTER TABLE 语句写成启动时的迁移脚本,比让客户手动跑 SQL 靠谱得多。
5. 避坑记录:跨版本和嵌入式数据库的翻车现场
5.1 Unicode 迁移后中文乱码
现象:D7 工程迁到 D10.3,老数据里的中文显示成乱码或者问号,写入的新中文也读不出来。 原因:D7 时代 string 是 ANSI 编码,写入文件时字节流按本机代码页存储;D2009 后 string 是 Unicode,引擎对字段字符串做了编码转换,两边读出来的字节对不上。 解决:老库数据先用旧的 D7 程序导出,中间文件用 UTF-8 编码导出再导入;或者建库时把字符字段声明为 BLOB/MEMO 避免引擎做编码转换,前者更通用。注意转换前先备份整个 .abs 文件。
5.2 加密文件打不开或误报损坏
现象:设置了 EncryptionPassword 的库,换台机器打开报「文件头无法识别」,让人误以为文件损坏。 原因:密码或加密算法版本不一致。引擎把加密密码的哈希写进文件头,密码不对时文件头解析直接失败,不是校验和错误,是连格式都对不上。 解决:确认两件事——同一套 v7.93 的引擎做读写;连接属性里重新指定相同密码。不要相信「随便输个密码试试」,连续试错不会解锁,只会浪费时间。预防做法是建库时把密码同时写进 config.ini 的注释里,给客户也留一份。
5.3 网络共享目录并发写导致文件损坏
现象:局域网共享盘上跑多客户端,某客户端写入时程序卡死,重启后库文件打不开,检查工具也修不回来。 原因:这套组件是为单用户写入设计的,共享模式下多进程同时写同一文件,事务日志互相覆盖,崩溃后无法恢复。它支持共享读,但写入方只能有一个。 解决:架构上分离读写——数据文件放服务器,业务写入用单一服务进程(或定时任务写入),客户端只读。小团队可以用「单文件锁 + 串行写」的土办法,但终究不如换个真正的网络数据库。别在共享目录上开「脱机文件」,那是隐藏的坑,会让日志滞后落盘。
5.4 多线程读写假死
现象:主线程操作库正常,放 TThread 里读数据时界面卡死或报「Dataset not in edit mode」。 原因:嵌入引擎的连接对象不是线程安全的,单连接多线程并发会让内部游标状态错乱,锁等待时又没释放主线程。 解决:每个线程创建独立的 TABSDatabase/TABSTable 连接,只读线程对同一个文件打开只读连接没问题;写操作全局串行,用一个 TCriticalSection 保护连接。我一般把数据库访问封进一个「DB Service」单例,内部用锁排队,UI 层永远不直接持有数据集。
5.5 IDE 装包顺序错误导致编译失败
现象:D11 里先安装 Design.dpk,编译报找不到 ABSDB.dcu,包管理器里显示红色感叹号。 原因:设计时包依赖运行时包的输出,运行时包没编译,DCU 文件还没生成,设计时包自然找不到符号。 解决:始终「先编译运行时包,再编译设计时包」,顺序写进自己的装包笔记。换电脑重装时同样按这个顺序,别因为上次装过就凭感觉跳步。
5.6 排查手段:从日志到文件级观察
提示:嵌入式数据库出问题时,第一反应别重装,先备份当前库文件。
排查问题我一般按三层走。第一层开日志:组件事务类支持日志输出,写操作前把 SQL 打到文本日志里,能确认是不是业务层写坏了数据。第二层看文件:用系统工具观察 .abs 文件的句柄占用和读写偏移,能定位是不是多进程同时写了同一个文件。第三层做还原:把问题库复制一份,在隔离环境里用官方 Demo 打开,确认是引擎问题还是数据结构问题。做完这三步再去处理业务代码,基本不会白忙。
6. 二次开发:给这套库加一层自己的适配器
6.1 封装 IDatabase 接口
嵌入式数据库最大的问题是被绑死在组件上。我的做法是先封一层接口,业务代码只依赖于接口,不直接引用 TABSDatabase 的类型。
type IDatabase = interface procedure Connect(const AFileName, APassword: string); function OpenQuery(const ASQL: string): TDataSet; procedure Execute(const ASQL: string); procedure StartTransaction; procedure Commit; procedure Rollback; end; TAbsDatabaseAdapter = class(TInterfacedObject, IDatabase) private FDB: TABSDatabase; public procedure Connect(const AFileName, APassword: string); function OpenQuery(const ASQL: string): TDataSet; procedure Execute(const ASQL: string); procedure StartTransaction; procedure Commit; procedure Rollback; end;业务层只依赖 IDatabase,以后要换 SQLite 或 MySQL,只需要新增一个 Adapter 类,界面代码零改动。这是我在老工程改造里收益最大的一步。
6.2 迁移脚本与自增主键
老工程改数据结构是家常便饭,我的做法是启动时跑一个版本号表,按版本号依次执行 ALTER TABLE 脚本。这套组件的 DDL 里,INTEGER PRIMARY KEY 自带自增行为,不需要额外 AUTOINCREMENT 关键字,写迁移脚本时别照搬 SQL Server 的写法。
6.3 验证方法
每次改完适配层,我强制走一遍回归:建临时库跑增删改查、开事务中断验证回滚、加密读写一轮、拷贝文件到新目录验证部署。数据量测试也要做——用循环插入 10 万行模拟真实负载,确认查询索引生效。从那以后我每次接手老工程,都强制先建临时库、把 SQL 全部过一遍再动结构,宁可多花半天也不让客户拿生产数据试错。这套 v7.93 源码我反复拆过几遍,最大的收获不是 API,而是「嵌入式数据库也好、C/S 也好,先想清楚写入边界再写代码」这个习惯,希望帮到你。
本文还有配套的精品资源,点击获取