DbContext
Enterprise cross-platform ORM framework with ULog logging system
- Active · last change 5-25-2026
- ★ 1 on GitHub
README
DbContext — Delphi 轻量级 ORM 框架
Delphi 11.3 · FireMonkey · SQLite · Code First(仿 Entity Framework Core) 更新日期:2026-05-24 | 当前版本:v2.9
一、设计理念
一个 Context 管所有实体,统一入口、统一提交、自动内存管理。
创建 TDbContext 实例后,所有 CRUD 操作围绕它展开——要么直接用 TDbContext.DbSet<T>,要么继承它声明 TDbSet<T> 属性以便 IDE 代码补全。实体生命周期由框架统一管理,New 创建、Select 查询返回的对象全部纳入 FManagedObjects,调用方只持有引用,无需手动 Free。
核心能力一览
| 特性 | 说明 |
|---|---|
| 零配置入口 | TDbContext.Create('mydb.db') 后直接 DbSet<T> 开写,无需声明子类 |
| 编译期安全查询 | @v.Field 指针式属性引用,字段改名 → 编译报错,杜绝字符串拼写错误 |
| 自动变更追踪 | 五态模型 + 快照对比,UPDATE 仅含真正变更的列 |
| 统一事务提交 | N 个不同类型实体的增删改 → 一句 SaveChanges 全部落地 |
| 自动内存管理 | FManagedObjects 统一回收,调用方只持有引用,永远不需要 Free |
| 可重入线程锁 | 同一线程嵌套调用不卡自己(递归临界区) |
| 导航属性预加载 | IncludeNav 批量填充,避免 N+1 查询 |
| Snowflake 分布式 ID | 64 位全局唯一,1024 节点,4096 IDs/ms,含时钟回拨检测 |
| 乐观并发控制 | [Timestamp] 行版本号,冲突抛 DbUpdateConcurrencyException |
二、快速入门
2.1 定义实体
uses
UValidationAttributes, UEntityList;
type
// ——— 前置声明(解决循环引用)———
books = class;
zhu_ming = class;
// ——— Author(一方)———
Author = class
private
FId: Integer;
Fname: string;
FregTime: TDateTime;
Fguid: string;
Fbooks: TEntityList<books>;
published
[Key]
property Id: Integer read FId write FId;
[StringLength(50)]
[Required]
property name: string read Fname write Fname;
[AutoDefaultValue('now')]
property regTime: TDateTime read FregTime write FregTime;
[GuidKey]
property guid: string read Fguid write Fguid;
[OneToMany]
property books: TEntityList<books> read Fbooks write Fbooks;
end;
// ——— books(多方 — 同时关联 Author 和 zhu_ming)———
books = class
private
FId: Integer;
Fname: string;
Fprice: Double;
Fauthor: Author;
Fzhus: zhu_ming;
public
[Key]
property Id: Integer read FId write FId;
[StringLength(50)]
property name: string read Fname write Fname;
property price: Double read Fprice write Fprice;
[OneToOne]
property author: Author read Fauthor write Fauthor;
[OneToOne]
property zhus: zhu_ming read Fzhus write Fzhus;
end;
// ——— zhu_ming(分组实体,一方指向多个 books)———
zhu_ming = class
private
FId: Integer;
Fming: string;
FConfigId: Integer;
Fbooks: TEntityList<books>;
Fgd: string;
public
[Key]
property Id: Integer read FId write FId;
[StringLength(20)]
property ming: string read Fming write Fming;
property ConfigId: Integer read FConfigId write FConfigId;
[OneToMany]
property books: TEntityList<books> read Fbooks write Fbooks;
[SnowId]
property gd: string read Fgd write Fgd; // 自动雪花 ID
end;
// ——— config(映射到 config 表)———
config = class
private
FId: Integer;
FtblVer: string;
FappVer: string;
public
[Key]
property Id: Integer read FId write FId;
[StringLength(20)]
property tblVer: string read FtblVer write FtblVer;
[StringLength(20)]
property appVer: string read FappVer write FappVer;
end;2.2 两种使用入口
方式 A:直接使用 TDbContext(无需子类,零声明)
var db := TDbContext.Create('mydb.db');
// 任意 T 即刻可用 — OneDbSet 动态创建表、注册 TDbSet
var list := db.DbSet<Author>.Where('name', '=', '金庸').Select;
db.DbSet<books>.Insert(aBook);
db.SaveChanges;方式 B:继承 TDbContext 声明属性(IDE 代码补全 + 编译期类型检查)
type
TDb = class(TDbContext)
private
FAuthors: TDbSet<Author>;
Fbooks: TDbSet<books>;
Fzhu_mings: TDbSet<zhu_ming>;
Fconfigs: TDbSet<config>;
public
property Authors: TDbSet<Author> read FAuthors;
property books: TDbSet<books> read Fbooks;
property zhu_mings: TDbSet<zhu_ming> read Fzhu_mings;
property configs: TDbSet<config> read Fconfigs;
end;
var db := TDb.Create('mydb.db'); // 自动建表,扫描 TDbSet<T> 属性
db.Debug := True; // 开启 SQL 日志三、CRUD 完整操作
3.1 新增 Insert
框架中有 两种新增路径,适用不同场景:
标记式插入(推荐 — 经 SaveChanges 统一事务)
// 单个实体
var author := db.Authors.New; // New 创建,自动纳入 FManagedObjects 管理
author.name := '金庸';
db.Authors.Insert(author); // 标记 esAdded(不执行 SQL)
db.SaveChanges; // 统一提交 → INSERT → 回填自增 Id
// 批量混合插入 — N 个不同类型实体,一个事务搞定
var a1 := db.Authors.New;
a1.name := '金庸';
db.Authors.Insert(a1);
var a2 := db.Authors.New;
a2.name := '古龙';
db.Authors.Insert(a2);
var b1 := db.books.New;
b1.name := '射雕英雄传';
b1.price := 99.80;
db.books.Insert(b1);
db.SaveChanges; // 三条实体 原子提交级联新增(导航属性自动追踪 + 父子依赖自动排序)
var author := db.Authors.New;
author.name := '金庸';
var book1 := db.books.New;
book1.name := '射雕英雄传';
book1.price := 99.80;
author.books.Add(book1); // 自动设置 FK、标记 esAdded
var book2 := db.books.New;
book2.name := '天龙八部';
book2.price := 89.00;
author.books.Add(book2);
db.SaveChanges;
// 执行顺序:
// ① Author INSERT → 回填 Author.Id
// ② 同步 books.author 的 FK 值
// ③ books 批量多行 INSERT: (name,price,author_id) VALUES (...), (...)
// ④ 回填 books.Id直接执行式插入(不经过 SaveChanges,立即执行 SQL)
// 批量同条件 — 一条 SQL 直接完成
db.books.Where('name', '=', '无用的书').Insert;NewInsert 快捷方法(New + Insert 一步完成)
// 等价于: var a := db.Authors.New; db.Authors.Insert(a);
var a := db.Authors.NewInsert; // New → Insert 一气呵成
a.name := '金庸';
db.SaveChanges;Dynamic 实体插入(无预先声明 TDbSet 属性,运行时分辨率表)
var ds := db.DbSet<newTest>; // 自动建表 + 注册
var nt := ds.New;
nt.name := '动态实体测试';
ds.Insert(nt);
db.SaveChanges;3.2 查询 Select / First / Last
// ——— 全表查询 ———
var allAuthors := db.Authors.Select; // TList<Author>,框架持有无需 Free
for var a in allAuthors do
Log.d(a.name);
// ——— 匿名方法过滤(内存级 Lambda)———
var filtered := db.zhu_mings.Select(
function(z: zhu_ming): Boolean
begin
Result := (z.Id > 5) and z.ming.Contains('分组');
end);
// ——— 链式查询(字符串式 WHERE)———
var result := db.zhu_mings
.Where('ming', '=', '武侠分组')
.OrderBy('Id')
.Limit(10, 0)
.Select;
// ——— 链式查询(指针式 WHERE — 编译期安全)———
var v: zhu_ming;
var result2 := db.zhu_mings
.Ref(v) // ← 必先 Ref 注册实例
.Where(@v.ming, '=', '武侠分组') // @v.Field 改名即编译报错
.AndWhere(@v.Id, '>', 5)
.OrderByDesc(@v.Id)
.Limit(20, 0)
.Select;
// ——— AsNoTracking 查询(不纳入 Tracker,省内存)———
var noTrack := db.zhu_mings
.AsNoTracking
.Where(@v.Id, '>', 50)
.Select;
// 返回实体不占用 esUnchanged 条目,Select 后自动清理
// ——— 首条 / 末条 ———
var first := db.configs.First;
var last := db.configs.Last;
// ——— 存在性 / 全值判断 ———
if db.configs.Any then ...
if db.configs.All(function(c: config): Boolean begin Result := c.Id > 0; end) then ...3.3 修改 Update
标记式(追踪模式 — 仅更新变更的列)
var z := db.zhu_mings.First;
z.ming := '新名称'; // 改了一个属性
db.zhu_mings.Update(z); // 标记 esModified
db.SaveChanges; // UPDATE zhu_ming SET ming=:p WHERE Id=:pk
// 注意:如果还改了 ConfigId,UPDATE 会含两列;未变更的列不会出现在 SET 中链式批量(直接执行模式 — 不经过 SaveChanges)
// 字符串式 SET
db.zhu_mings
.Sets('ming', '批量更新名称')
.Where('Id', '>', 0)
.Update; // 立即执行 UPDATE
// 指针式 SET(编译期安全)
db.zhu_mings
.Sets(@v.ming, '批量更新')
.Where(@v.Id, '>', 5)
.Update;
// 多列同时 SET
db.books
.Sets(@b.name, '新书名')
.Sets(@b.price, 199.99)
.Where(@b.Id, '=', 42)
.Update;3.4 删除 Delete
标记式(追踪模式)
var z := db.zhu_mings.First;
db.zhu_mings.Delete(z); // 标记 esDeleted
db.SaveChanges; // DELETE FROM zhu_ming WHERE Id=:pk链式批量(直接执行模式)
// 单条件删除
db.zhu_mings.Where('Id', '>', 100).Delete; // 立即执行
// 组合条件删除
db.books
.Where(@b.name, '=', '无用的书')
.AndWhere(@b.price, '<', 10)
.Delete;3.5 追踪模式 vs 直接执行 对照表
| 操作 | 追踪模式(经 SaveChanges) | 直接执行模式 |
|---|---|---|
| Insert | DbSet.Insert(e) → SaveChanges |
DbSet.Where(...).Insert |
| Update | DbSet.Update(e) → SaveChanges |
DbSet.Sets(...).Where(...).Update |
| Delete | DbSet.Delete(e) → SaveChanges |
DbSet.Where(...).Delete |
追踪模式适用场景:N 个不同类型实体、写入后需要操作对象(如回填 Id 后继续用)、导航属性级联操作
直接执行模式适用场景:批量同条件操作、不需要加载实体到内存、一条 SQL 高性能执行
四、WHERE 条件构建(四种模式混用)
4.1 指针式 — @v.Field(编译期安全)
必须先 Ref(v) 注册实例,然后通过 @v.FieldName 引用属性。本质是取字段内存地址 → 反查属性名 → 参数化 SQL。
var v: zhu_ming;
db.zhu_mings
.Ref(v) // 注册实例,解析 @v 地址
.Where(@v.ming, '=', '武侠分组') // @v.ming → 编译期安全
.AndWhere(@v.Id, '>', 10)
.OrWhere(@v.ming, '=', '备用分组')
.Select;字段重命名时:改 Fming → FgroupName,@v.ming 编译直接报错,无需全局搜索字符串。
Ref 何时需要:只要当前链中用到了 @v.Field,就需要先 Ref(v)。一次 Ref 后,后续所有 @v 都生效。
4.2 字符串式
db.zhu_mings
.Where('ming', '=', '武侠分组')
.AndWhere('Id', '>', 10)
.OrWhere('ming', '=', '备用分组')
.Select;4.3 TExpW 链式表达式 — 编译期安全的复杂条件
适用于带括号嵌套的复杂 WHERE。两种风格:
风格 1:链式构建 — 字段 + 操作符 + 值逐步搭建
var v: zhu_ming;
var exp := db.ExpW<zhu_ming>(v, True); // True = 参数化 SQL
exp.f(@v.ming).Eq.Vs('武侠分组') // WHERE ming = :p1
._And
.f(@v.Id).Gr.Vi(10); // AND Id > :p2
// 括号嵌套:(ming='A' OR ming='B') AND Id > 0
exp.LArc
.f(@v.ming).Eq.Vs('A')._Or.f(@v.ming).Eq.Vs('B')
.RArc
._And
.f(@v.Id).Gr.Vi(0);
var result := db.zhu_mings.Where(exp).Select;风格 2:快捷组合 — 一句完成字段 + 操作符 + 值
var v: zhu_ming;
var exp := db.ExpW<zhu_ming>(v);
exp.Eq(@v.ming, '测试') // = '测试'
.A.Gr(@v.Id, 10) // AND > 10
.A.LeEq(@v.Id, 100); // AND <= 100
var result := db.zhu_mings.Where(exp).Page(1, 8).Select;TExpW 操作符速查表
| 操作符 | 等效 SQL | 快捷组合 | 链式写法 |
|---|---|---|---|
Eq |
= | exp.Eq(@v.Id, 10) |
exp.f(@v.Id).Eq.Vi(10) |
NotEq |
<> | exp.NotEq(@v.Id, 10) |
exp.f(@v.Id).NotEq.Vi(10) |
Gr |
> | exp.Gr(@v.Id, 5) |
exp.f(@v.Id).Gr.Vi(5) |
GrEq |
>= | exp.GrEq(@v.Id, 10) |
exp.f(@v.Id).GrEq.Vi(10) |
Le |
< | exp.Le(@v.Id, 100) |
exp.f(@v.Id).Le.Vi(100) |
LeEq |
<= | exp.LeEq(@v.Id, 100) |
exp.f(@v.Id).LeEq.Vi(100) |
Like |
LIKE | exp.Like(@v.ming, '%武侠%') |
exp.f(@v.ming).Like.Vs('%武侠%') |
_And / A |
AND | exp.Eq(...).A.Gr(...) |
exp.f(...)._And.f(...) |
_Or / O |
OR | exp.Eq(...).O.Eq(...) |
exp.f(...)._Or.f(...) |
LArc / RArc |
( / ) | exp.LArc.Eq(...).O.Eq(...).RArc |
值类型快捷方法
| 方法 | 对应 TValue 类型 | 示例 |
|---|---|---|
Vs |
string | .Eq.Vs('武侠分组') |
Vi |
Integer | .Gr.Vi(10) |
Vf |
Double | .LeEq.Vf(99.80) |
Vb |
Boolean | .Eq.Vb(True) |
V |
通用 TValue | .Eq.V(SomeValue) |
4.4 高级条件
// IN 子句
db.zhu_mings.WhereIn('Id', [1, 2, 3, 5, 8]);
db.zhu_mings.WhereNotIn('Id', [99, 100]);
// BETWEEN
db.zhu_mings.WhereBetween('Id', 1, 100);4.5 原始 SQL WHERE
db.zhu_mings.Where('ming LIKE "%武侠%" AND Id > 10').Select;五、排序、分页、分组、去重
var v: zhu_ming;
// ——— 排序 ———
db.zhu_mings.OrderBy('Id'); // 字符串
db.zhu_mings.OrderBy(@v.Id); // 指针
db.zhu_mings.OrderByDesc('Id');
db.zhu_mings.OrderByDesc(@v.Id);
// ——— 分页 ———
db.zhu_mings.Limit(10, 0); // LIMIT 10 OFFSET 0
db.zhu_mings.Page(1, 10); // 第 1 页 → LIMIT 10 OFFSET 0
db.zhu_mings.Page(2, 10); // 第 2 页 → LIMIT 10 OFFSET 10
// ——— 去重 ———
db.zhu_mings.Distinct;
// ——— 分组 + Having ———
db.zhu_mings.GroupBy('ConfigId');
db.zhu_mings.GroupBy(@v.ConfigId);
db.zhu_mings.GroupBy(@v.ConfigId).Having('COUNT(*) > 0');六、聚合函数
所有聚合函数支持 字符串式 和 指针式 字段引用,在已有 WHERE/Join 条件基础上执行:
| 方法 | 返回 | 示例 |
|---|---|---|
Count |
Integer | db.zhu_mings.Where(@v.Id, '>', 0).Count |
SUM |
Double | db.books.Where(@b.price, '>', 50).SUM(@b.price) |
MAX |
Variant | db.zhu_mings.MAX('Id') |
MIN |
Variant | db.zhu_mings.MIN(@v.Id) |
AVG |
Double | db.zhu_mings.AVG('Id') |
var v: zhu_ming;
var b: books;
// 查某分组下的数据条数
var count := db.zhu_mings.Where(@v.ming, '=', '武侠分组').Count;
// 查高价书的总金额
var total := db.books.Where(@b.price, '>', 50).SUM(@b.price);
// 查最大 Id
var maxId := db.zhu_mings.MAX(@v.Id);
// 聚合 + 分组
var avgPrice := db.books.GroupBy(@b.name).AVG('price');七、JOIN 操作
7.1 投影类(任意 class 接收 JOIN 结果)
JOIN 结果映射到任意 Delphi 类,属性名与 SELECT 列名自动匹配:
type
TZhuMingJoinConfig = class
Id: Integer; // ← 匹配 zhu_ming.Id
ming: string; // ← 匹配 zhu_ming.ming
tblVer: string; // ← 匹配 config.tblVer
end;7.2 字符串式 JOIN
// INNER JOIN — zhu_ming JOIN config ON ConfigId = Id
var result := db.zhu_mings
.Join<config>('ConfigId', 'Id', db.configs)
.SelectJoin<TZhuMingJoinConfig>;
for var r in result do
Log.d(Format('Id:%d ming:%s tblVer:%s', [r.Id, r.ming, r.tblVer]));
// LEFT JOIN
var result2 := db.zhu_mings
.LeftJoin<config>('ConfigId', 'Id', db.configs)
.SelectJoin<TZhuMingJoinConfig>;7.3 指针式 JOIN(编译期安全)
var vzJ: zhu_ming;
var vcJ: config;
var result := db.zhu_mings
.Ref(vzJ) // ← 主表 Ref
.JoinModel<config>(vcJ) // ← 第二表注册
.Join<config>(@vzJ.ConfigId, @vcJ.Id, db.configs)
.SelectJoin<TZhuMingJoinConfig>;7.4 Include / LeftInclude 语法糖
自动匹配外键列,三级回退策略:
[ForeignKey('table', 'column')]注解 → 最优先- 命名约定
ConfigId→ 自动匹配config.Id - 同名
[Key]属性 → 兜底
// INNER JOIN(自动 FK 匹配)
var result := db.zhu_mings
.Include<config>(db.configs)
.SelectJoin<TZhuMingJoinConfig>;
// LEFT JOIN(自动 FK 匹配)
var result2 := db.zhu_mings
.LeftInclude<config>(db.configs)
.SelectJoin<TZhuMingJoinConfig>;八、IncludeNav 导航属性预加载
在 Select/First/Last 之前声明,框架批量填充导航属性,避免 N+1 查询。
8.1 使用方式
// 加载 Author 的 books 子实体(一对多)
var authors := db.Authors
.IncludeNav('books')
.Select;
// 访问 authors[0].books[0].name — 无需额外查询,已全部填充
// 加载 books 的 author 父实体(一对一)
var bookList := db.books
.IncludeNav('author')
.Select;
// booksList[0].author.name — 已填充
// 同时加载多个导航属性
var bookList2 := db.books
.IncludeNav('author')
.IncludeNav('zhus')
.Select;8.2 内部机制
??????????????????1 ??? SQL???
???
FillOneToMany ??? ??????????????? PK ?????? ??? WHERE FK IN (...) ??????????????????
???
FillOneToOne ??? ??????????????? FK ?????? ??? WHERE PK IN (...) ??????????????????
???
??????????????? Tracker (esUnchanged)???FK ???????????? ??? ?????????????????????
???
??????????????????????????? esUnchanged ?????? ??? ?????? Tracker ??????九、集合运算
集合运算在内存中执行(先执行双方 SQL 查出数据 → 再比对),适用于不同条件下的结果集合并/交叉/差集:
var v: zhu_ming;
// ——— 并集 — 条件 A + 条件 B 的全部数据 ———
var unionResult := db.zhu_mings
.Where(@v.Id, '<', 10) // 查询 A
.Union(
db.zhu_mings.Where(@v.Id, '>', 5) // 查询 B
); // A ∪ B → Id < 10 或 Id > 5
// ——— 交集 — 同时满足条件 A 和条件 B ———
var interResult := db.zhu_mings
.Where(@v.Id, '<', 10) // {1..9}
.Intersect(
db.zhu_mings.Where(@v.Id, '>', 5) // {6..N}
); // A ∩ B → {6,7,8,9}
// ——— 差集 — 在 A 但不在 B ———
var exceptResult := db.zhu_mings
.Where(@v.Id, '<', 10) // {1..9}
.ExceptSet(
db.zhu_mings.Where(@v.Id, '>', 5) // {6..N}
); // A \ B → {1,2,3,4,5}十、变更追踪系统
10.1 五态模型
| 状态 | 枚举值 | 来源 | SaveChanges 行为 |
|---|---|---|---|
esUnknown |
0 | 初始值 | 跳过 |
esAdded |
1 | Insert() 标记 |
生成 INSERT |
esModified |
2 | Update() 标记 或 自动检测 |
生成 UPDATE(仅变更列) |
esDeleted |
3 | Delete() 标记 |
生成 DELETE |
esUnchanged |
4 | Select/First/Last 查询后 |
自动检测变更,无变更则跳过 |
10.2 自动 dirty check 流程
SaveChanges 时遍历所有 esUnchanged 实体:
???????????????????????? vs ??????
??? ????????? ??? ?????? esModified
??? ????????? ??? ?????? esUnchanged?????????
???????????? FK ????????? vs ??????
??? ????????? ??? ?????? esModified
?????? UPDATE ????????? dirty ???????????????????????????具体效果:一个 zhu_ming 实体有 6 个属性 + 2 个 FK,只改了 ming(字符串字段)→ UPDATE 语句只含一列:UPDATE zhu_ming SET ming=:p WHERE Id=:pk。
10.3 SaveChanges 完整执行管线
AcquireLock ??? ??????????????????
???
SyncOneToOneFKs ??? ???????????? PK ??? FK ??????
????????????????????? vs ????????????????????????????????????
???
AutoDirtyCheck ??? esUnchanged ???????????????????????????
???
CheckChange ??? ??????????????????????????????????????????
???
ValidateEntities ??? ?????? esAdded + esModified????????? [Required]/[StringLength] ??????
???
ExecuteModifications ??? UPDATE ??????????????? ??? DELETE ????????????
???
AutoIncIdInsert ??? ??? ???????????????????????????
??? ?????????????????? INSERT INTO (...) VALUES (...),(...),...
??? ???????????? Id ???????????????
??? ?????? OneToMany FK
??? ?????????????????? INSERT
??? ?????????????????? Id
???
Commit / Rollback ??? ???????????? + ???????????????????????? Id ???
???
ReleaseLock十一、数据注解 / 验证属性
11.1 全部注解速查
| 注解 | 建表 DDL 效果 | 验证行为 | 适用类型 |
|---|---|---|---|
[Key] |
PRIMARY KEY AUTOINCREMENT NOT NULL |
— | Integer |
[Primary] |
PRIMARY KEY |
— | 任意 |
[Required] |
NOT NULL |
非空 + Trim 后不为空 | string |
[NotNull] |
NOT NULL |
— | 任意 |
[StringLength(20)] |
VARCHAR(20) |
字符串长度 ≤ 20 | string |
[StringLength(2, 50)] |
VARCHAR(50) |
长度 ∈ [2, 50] | string |
[DecimalNum(10, 2)] |
REAL |
必须为浮点类型 | Double |
[DefaultValue('0')] |
DEFAULT 0 |
— | Integer |
[RegExCheck('\d+')] |
— | 正则匹配测试 | string |
[ForeignKey('Table','Col')] |
—(元数据) | — | Integer |
[OneToOne] / [OneToMany] |
自动在对应表补建 FK 列 | — | 导航属性 |
[GuidKey] |
— | 自动赋值无横线 GUID | string |
[SnowId] |
— | 自动赋值 Snowflake Int64 ID | string / Int64 |
[AutoDefaultValue] |
— | 自动填充 TDateTime.Now |
TDateTime |
[NotMapped] |
跳过建列 | 跳过追踪/映射/快照 | 任意 |
[Column('colName')] |
属性名→列名映射 | — | 任意 |
[Timestamp] |
— | 行版本号递增 + RowsAffected 校验 | Integer |
[Index(True)] |
CREATE UNIQUE INDEX |
—(需手动调用) | 任意 |
[AutoFormat('Method')] |
— | 调用实体方法转换属性值后存入 DB | 任意 |
11.2 验证方式
自动验证 — SaveChanges 内建管线自动执行:
var a := db.Authors.New;
a.name := ''; // 违反 [Required]
db.Authors.Insert(a);
var n := db.SaveChanges; // n = 0,ValidateEntities 拦截,事务回滚手动验证 — 提前检查单个实体:
var cfg: config;
var msgs: TStringList;
msgs := TStringList.Create;
try
if not db.configs.ValidModel(cfg, msgs) then
for var msg in msgs do
Log.e(msg); // "字段 tblVer: 字符串长度不能超过 20"
finally
msgs.Free;
end;11.3 [AutoFormat] 属性值自动格式转换
作用 — SaveChanges 进入状态检测前,自动调用指定的实体方法,将当前属性值作为参数传入,用方法的返回值覆盖该属性,然后再写入数据库。
执行时机 — 处于 SaveChanges 管线的 SyncOneToOneFKs 之后、HasChanges 自动脏检之前:
SyncOneToOneFKs ??? AutoFormat ?????????????????? ??? HasChanges ??? Validate ??? INSERT/UPDATE两种调用模式
| 模式 | 签名 | 调用方式 | 适用场景 |
|---|---|---|---|
| 无参 | function Xxx: TResult; |
m.Invoke(obj, []) |
自动生成值,不依赖当前属性值 |
| 单参 | function Xxx(const val: TType): TResult; |
m.Invoke(obj, [当前值]) |
基于当前值做变换 |
type
TUser = class
private
FPassword: string;
FUpdatedAt: TDateTime;
FCreatedAt: TDateTime;
public
// 单参模式 — 传入当前 Password 值,返回哈希
function HashPassword(const raw: string): string;
// 无参模式 — 返回当前时间
function GetNow: TDateTime;
[AutoFormat('HashPassword')]
property Password: string read FPassword write FPassword;
[AutoFormat('GetNow')]
property UpdatedAt: TDateTime read FUpdatedAt write FUpdatedAt;
[AutoFormat('GetNow')]
property CreatedAt: TDateTime read FCreatedAt write FCreatedAt;
end;
implementation
function TUser.HashPassword(const raw: string): string;
begin
// ★ 关键:幂等性判断 — 已是哈希则跳过
if raw.StartsWith('$2a$') then
Exit(raw);
Result := TBcrypt.HashPassword(raw);
end;
function TUser.GetNow: TDateTime;
begin
Result := Now;
end;使用效果
var user := db.Users.New;
user.Password := '123456';
user.Name := 'Alice';
db.Users.Insert(user);
db.SaveChanges;
// → INSERT INTO Users (Password, Name, UpdatedAt, CreatedAt)
// VALUES ('$2a$10$...', 'Alice', '2026-05-25 10:30:00', '2026-05-25 10:30:00')
// 后续只改 Name,不改 Password:
user.Name := 'Bob';
db.Users.Update(user);
db.SaveChanges;
// → UPDATE Users SET Name='Bob', UpdatedAt='2026-05-25 10:31:00' WHERE Id=1
// Password 未变 → 不进入 SET(脏检跳过)
// UpdatedAt 被 GetNow 刷新 → 脏检感知 10:30≠10:31 → 进入 SET核心注意事项
1. 确定性转换方法必须幂等
这是 [AutoFormat] 最重要的设计契约。框架在每次 SaveChanges 都会对所有被跟踪实体执行转换,无论属性是否被用户修改。如果方法不幂等,已转换的值会被再次转换,产生错误结果。
// ❌ 错误 — 不幂等,每次 SaveChanges 都会重哈希
function TUser.HashPassword(const raw: string): string;
begin
Result := SHA256(raw); // SHA256(SHA256(raw)) ≠ SHA256(raw)
end;
// ✓ 正确 — 幂等,已是哈希则原样返回
function TUser.HashPassword(const raw: string): string;
begin
if Length(raw) = 64 then // SHA256 输出固定 64 字符
Exit(raw);
Result := SHA256(raw);
end;| 变换类型 | 是否幂等 | 示例 | 方法内需判断 |
|---|---|---|---|
ToUpper / ToLower |
天然幂等 | Result := raw.ToUpper; |
不需要 |
Trim |
天然幂等 | Result := raw.Trim; |
不需要 |
| 加密/哈希 | 需要保证 | HashPassword |
需要判断已加密则跳过 |
| 时间戳 | 天然非幂等 | GetNow |
不需要(每次刷新是预期行为) |
| 格式化 | 视情况 | 日期格式化 | 需要判断已是目标格式则跳过 |
2. 方法必须是 function,不能是 procedure
框架通过 m.Invoke 获取返回值并 SetValue 写回属性。如果是 procedure,Invoke 返回 TValue.Empty,属性会被清空。
3. 参数类型必须与属性类型匹配
单参模式下,方法参数的 TRttiType.Handle 必须与属性的 PropertyType.Handle 一致,否则抛异常。
4. 方法必须是 public 的实例方法
通过 TRttiType.GetMethod 查找,仅能访问公开方法。
与 [AutoDefaultValue] 的区别
| 特性 | [AutoDefaultValue] |
[AutoFormat] |
|---|---|---|
| 触发时机 | 实体初始化时一次性 | 每次 SaveChanges 都执行 |
| 输入 | 不依赖属性值 | 可接收当前属性值为参数 |
| 实现 | 直接赋值 | 调用指定方法 |
| 适用 | 固定默认值 | 需计算/转换/刷新的值 |
| 示例 | [AutoDefaultValue('now')] 初始化 regTime |
[AutoFormat('GetNow')] 每次刷新 UpdatedAt |
12.1 Snowflake 分布式 ID 生成器 [SnowId]
64 位全局唯一主键:[1 保留] [41 时间戳差 ms] [10 节点] [12 序列]
每毫秒 4096 个 ID / 节点,1024 个分布式节点,69 年可用。
type
TOrder = class
private
FOrderId: Int64;
public
[SnowId]
property OrderId: Int64 read FOrderId write FOrderId;
end;
// 多节点部署 — 启动时调用一次
TGenId.Init(3); // WorkerId=3,范围 0..1023
// 手动生成(不依赖实体注解时)
var id := TGenId.NextId; // 全局唯一 Int64
// 从 ID 反向提取时间戳 — 用于排序 / 调试
var dt := TGenId.ExtractDateTime(id);内部保护:
- 同一毫秒序列溢出 → 自动等待下一毫秒
- 系统时间被调回 → 检测并等待追上(>100ms 异常拒绝服务,避免 ID 重复)
- 线程安全:
TCriticalSection临界区保护
12.2 乐观并发控制 [Timestamp]
type
// 映射到 concurrency_test 表
concurrency_test = class
private
FId: Integer;
FName: string;
FVersion: Integer;
public
[Key]
property Id: Integer read FId write FId;
property Name: string read FName write FName;
[Timestamp]
property Version: Integer read FVersion write FVersion;
end;
// ——— 正常操作 ———
var entity := db.DbSet<concurrency_test>.First;
entity.Name := '新名称';
db.DbSet<concurrency_test>.Update(entity);
db.SaveChanges;
// 生成: UPDATE concurrency_test SET Name=:p1, Version=Version+1
// WHERE Id=:pk AND Version=:oldVersion
// RowsAffected=1 → 成功,Version 回填到 entity
// ——— 并发冲突 ———
// 线程 A: 读到 Version=3
// 线程 B: 先提交 Version=3 → DB 中 Version 变 4
// 线程 A: 仍带 Version=3 提交 → RowsAffected=0
// → 抛出 DbUpdateConcurrencyException("行已被其他事务修改")12.3 外部事务 API
框架内部默认自动事务管理,但也支持外部显式控制:
db.BeginTrans;
try
// ... 任意 CRUD 操作 ...
db.SaveChanges;
db.Commit;
except
db.Rollback;
raise;
end;12.4 线程安全
所有数据库操作受可重入临界区保护:
// 框架内部方法自动加锁: Select / First / Last / SaveChanges
// 手动加锁 — 确保多步操作原子性
db.AcquireLock;
try
var first := db.configs.First; // 不会重新加锁(递归锁同一线程)
var list := db.zhu_mings.Select; // 同上
db.SaveChanges; // 同上
finally
db.ReleaseLock;
end;同一线程嵌套调用不会自己卡自己。多线程并发访问同一 SQLite 文件受锁保护。
12.5 内存管理
实体生命周期完全由框架管理,调用方零心智负担:
New ???????????????
???
??????????????? FManagedObjects???TQueryable<T> ?????? TObjectList???
???
Select / First / Last ???????????????????????????
???
TQueryable<T>.Destroy ??? FManagedObjects.Free ??? ????????????????????????调用方只持有引用,永远不需要写 entity.Free。唯一例外:CloneModel 创建的克隆对象需调用方自行管理。
12.6 自动建表与 DDL 演进
框架初始化时自动执行(扫描 TDbSet<T> 属性或运行时 OneDbSet 创建):
- RTTI 读取实体注解 → 生成
CREATE TABLE IF NOT EXISTS(含列类型、约束、默认值) - 表已存在 → 检测新增列 →
ALTER TABLE ADD COLUMN [OneToOne]/[OneToMany]→ 自动在对方表补建 FK 列(命名为_{PropName}_{TargetClassName}_id_)[Index]→ 自动创建索引
示例:加一个新实体类,不用写任何 DDL,db.DbSet<T>.New 时就自动存在对应表。
12.7 原始 SQL 兜底
LINQ 无法覆盖的场景,直接跑 SQL:
// 查询 + 实体映射
var list := db.QuerySql<config>('SELECT * FROM config WHERE Id > 10');
for var c in list do
Log.d(c.tblVer);
// 查询 + JSON 输出
var json := db.QuerySql('SELECT Id, ming FROM zhu_ming LIMIT 3');
// → [{"Id":1,"ming":"武侠分组"}, ...]
// 打开 TFDQuery — 自由处理结果集
var q := db.OpenSql('SELECT COUNT(*) as cnt, MAX(Id) as maxId FROM zhu_ming', nil);
try
Log.d(Format('共 %d 条, 最大Id=%d', [q.FieldByName('cnt').AsInteger,
q.FieldByName('maxId').AsInteger]));
finally
q.Free;
end;
// DDL / DML 直接执行
db.ExecSql('CREATE TABLE IF NOT EXISTS __test(id INTEGER)');
db.ExecSql('DROP TABLE IF EXISTS __test');
db.ExecSql('CREATE INDEX IF NOT EXISTS idx_ming ON zhu_ming(ming)');
// 字段存在性检测
if db.configs.HasField('tblVer') then ...12.8 工具方法集
// ——— 深克隆(同类型,返回值纳入 FManagedObjects,无需手动 Free)———
var clone := db.configs.CloneModel(original);
// 跨类型属性拷贝(静态方法 — source/dest 均由调用方管理生命周期)
TDbContext.CloneModel<zhu_ming, TZhuMingSimple>(source, dest, 'Id,ConfigId');
// ——— JSON ↔ 实体 ———
var obj := TDbContext.JsonToObject<config>('{"Id":1,"tblVer":"v2.9"}');
// obj 需调用方手动释放
// ——— 元数据 ———
var sl := TStringList.Create;
try
TDbContext.TblClassToTStrings<config>(nil, sl, '');
// sl → ['Id','tblVer','appVer',...]
finally
sl.Free;
end;
var tblName := TDbContext.ClsTableName<config>; // → 'config'
// ——— 缓存控制 ———
db.configs.InvalidateCache; // 手动失效单表缓存
// ——— 最后插入行 Id ———
var lastId := db.LastInsertRowId;12.9 [Column] 属性名 → 列名解耦
type
// 映射到 column_test 表
column_test = class
public
[Key]
property Id: Integer read FId write FId;
[Column('book_name')] // 属性名 BookName,数据库列名 book_name
[StringLength(50)]
property BookName: string read FBookName write FBookName;
[Column('book_price')] // 属性名 BookPrice,数据库列名 book_price
property BookPrice: Double read FBookPrice write FBookPrice;
end;
// SELECT book_name, book_price FROM column_test
// @v.BookName 仍以属性名引用 → SQL 自动转为 book_name12.10 AsNoTracking 轻量查询
// 大量只读查询无需变更追踪时,省内存 + 省快照开销
var result := db.zhu_mings
.AsNoTracking
.Where(@v.Id, '>', 0)
.Select;
// 实体不进入 Tracker(不拍快照),Select 后自动清理
// 对 result 中实体的修改不会触发 Update十三、项目实体关系总览
实体清单
| 实体类 | 表名 | 关键注解 |
|---|---|---|
config |
config | [Key], [StringLength(20)] |
Author |
Author | [Key], [GuidKey], [AutoDefaultValue], [OneToMany] |
books |
books | [Key], [StringLength(50)], [OneToOne] × 2 |
zhu_ming |
zhu_ming | [Key], [StringLength(20)], [OneToMany], [SnowId] |
newTest |
newTest | [Key] |
concurrency_test |
concurrency_test | [Key], [Timestamp] |
column_test |
column_test | [Key], [Column] × 2, [SnowId] |
实体关系图
Author (1) ???????????? OneToMany ???????????? (N) books (N) ???????????? OneToOne ???????????? (1) Author
(N) ???????????? OneToOne ???????????? (1) zhu_ming
zhu_ming (1) ???????????? OneToMany ???????????? (N) books
zhu_ming ConfigId ???????????? ???????????? ???????????? config.Id (Include ?????????????????????)十四、项目结构
DbContext/
????????? Project1.dpr -- ???????????????19 ???????????????????????????
????????? Unit1.pas / Unit1.fmx -- ???????????????
????????? Db/ -- ORM ?????????
??? ????????? UDbContext.pas -- ?????????TDbContext???TQueryable<T>???TDbSet<T>
??? ??? CRUD???JOIN???Include/IncludeNav???????????????
??? ??? SaveChanges ???????????????CloneModel????????? SQL
??? ????????? UDatabase.pas -- ????????????????????????SQLite / FireDAC ???????????????
??? ????????? USqlBuilder.pas -- ????????? SQL ????????????????????????
??? ????????? UEntityTracker.pas -- ?????????????????????????????? + ???????????? + ?????? INSERT/UPDATE/DELETE SQL
??? ????????? UEntityList.pas -- ?????????????????? TEntityList<T>??????????????????????????????
??? ????????? UExpW.pas -- ???????????????????????? TExpW<T>????????? + ???????????????
??? ????????? UCacheLayer.pas -- LRU + TTL ???????????????
??? ????????? UValidationAttributes.pas -- ???????????? / ???????????? / Column ??????
??? ????????? UGenId.pas -- Snowflake ID ????????????TSnowflakeGen + TGenId ?????????
??? ????????? UMytbl.pas -- ?????????????????????config / zhu_ming / Author / books ??????
??? ????????? UDb.pas -- DbContext ????????????????????????????????????
????????? pas/ -- ????????????
??? ????????? UTValueHelper.pas
????????? LogPas/ -- ?????????????????????
??? ????????? ULog.pas -- 5 ????????? + ???????????? + ?????? Flush + GetLogContent
??? ????????? UWebLog.pas -- Web ????????????
????????? readme.md十五、已知限制
- 单列主键 — 主键定位硬编码属性名
Id+[Key]注解,不支持复合主键 [AutoDefaultValue]— 目前仅覆盖TDateTime类型(自动填充Now),非标准类型需手动赋值,其自己可以找到关键点,自行扩展SUM返回 Double — Integer 列 SUM 仍返回 Double,需手动Trunc转换- IncludeNav 不支持多级级联 —
a.books.zhus.ming不起效,需逐层手动加载 - 多进程不协调 — 并发控制基于进程内
TCriticalSection,多进程/多实例访问同一 SQLite 文件需自行协调 - 单一数据库方言 — 当前仅 SQLite / FireDAC;多方言扩展点预留(
TSqlBuilder抽象了操作符,替换YDatabase为接口即可) GroupBy副作用 — 执行前会清除链上已有的OrderBy子句HasField上下文依赖 — 某些 TDbSet 上下文下返回异常,仅建议在已操作过的 DbSet 上使用
十六、版本历史
| 版本 | 日期 | 内容 |
|---|---|---|
| v2.9 | 2026-05-24 | Snowflake ID 生成器 + [SnowId] 自动赋值 + 自动补列修复 + 多线程测试 |
| v2.8 | 2026-05-23 | [Column] 属性名→列名映射注解 + column_test 实体 |
| v2.7 | 2026-05-22 | [Timestamp] 乐观并发控制 + 外部事务 API + concurrency_test 实体 |
| v2.6 | 2026-05-22 | AsNoTracking 轻量查询 + MarkAdded 修复 + NewInsert 入口 |
| v2.5 | 2026-05-23 | CloneModel 双重释放修复 + Tracker RemoveUnchanged 查询膨胀清理 + SaveChanges 诊断瘦身 + SyncOneToOneFKs 首次填充与变更区分 + FillOneToMany 影子 FK 快照固化 |
| v2.4 | 2026-05-22 | SaveChanges 内建验证 + Primary.Create 修复 + 遗留文件清理 |
| v2.3 | 2026-05-22 | Insert 导航实体自动追踪 + 子表依赖排序 + CreateTable tkClass 修复 |
| v2.2 | — | 变更自动追踪 + 父实体联动 + OneDbSet 独立运行 |
| v2.1 | — | 导航属性 FK 自动同步 + 变更跟踪修复 |
| v2.0 | — | 流式 JOIN/集合运算 + Include 自动 FK 匹配 + 编译期安全 @v.Field 指针式查询 |
| v1.0 | — | 初始版本 |

