简介
本文件为 DouPHP 框架 ORM 系统的权威技术文档,聚焦 Model 基类、查询构建器 Builder、集合 Collection、关系模型 Relation 及其实现(HasOne/HasMany/BelongsTo/BelongsToMany)、类型转换 CastResolver、声明式预加载 PrefetchRunner。文档从 ActiveRecord 模式出发,解释属性映射、查询构建、关系定义、事件系统与持久化流程,并给出与底层数据库连接层的交互方式及性能优化策略(预加载、懒加载、批量操作等)。
项目结构
ORM 子系统位于 core/orm 目录,采用分层清晰的设计:
- 模型层:Model 抽象基类,提供 ActiveRecord 能力、属性映射、事件、全局作用域、时间戳、多语言字段等。
- 查询层:Builder 封装链式查询,叠加 eager load、prefetch、全局作用域应用与结果水合。
- 集合层:Collection 对 Model 集合进行迭代、过滤、键控、序列化等。
- 关系层:Relation 抽象 + HasOne/HasMany/BelongsTo/BelongsToMany 具体实现。
- 类型转换:CastResolver 统一读写方向类型转换。
- 预加载:PrefetchRunner 基于 $prefetchers 声明式批量预热 URL、语言、附件等。
graph TB
subgraph "ORM 核心"
M["Model"]
B["Builder"]
C["Collection"]
R["Relation(抽象)"]
H1["HasOne"]
H2["HasMany"]
BT["BelongsTo"]
BM["BelongsToMany"]
CR["CastResolver"]
PR["PrefetchRunner"]
end
M --> B
B --> C
M --> R
R --> H1
R --> H2
R --> BT
R --> BM
M --> CR
B --> PR
核心组件
- Model:ActiveRecord 基类,负责表名、主键、fillable、casts、with、prefetchers、appends、translatable、事件、全局作用域、连接解析、属性访问/修改、实例工厂、静态入口、保存/删除等。
- Builder:在底层 Connection 之上提供链式查询、全局作用域应用、eager load、prefetch、结果水合、分页、聚合方法透传。
- Collection:对 Model 集合的遍历、过滤、映射、键控、提取字段、JSON 序列化等。
- Relation 族:统一惰性取值与批量预加载契约,支持一对一、一对多、多对一、多对多。
- CastResolver:统一类型转换(读/写),内置基础类型、日期时间、附件、多语言数据等。
- PrefetchRunner:按 $prefetchers 声明批量预热 URL、语言、附件、首图等,消除 N+1。
架构总览
ORM 以 Model 为中心,通过 Builder 组合查询条件并委托到底层 Connection;读取路径在 Builder::get/first/find/paginate 中完成水合、eager load、prefetch 与 afterHydrate 回调;写入路径通过 fill/save/create/destroy/update 等完成事务性持久化;关系通过 Relation 抽象统一惰性/批量加载;类型转换在属性访问时透明生效;预加载机制在批量读取后集中执行,避免 N+1。
sequenceDiagram
participant App as "业务代码"
participant Model as "Model"
participant Builder as "Builder"
participant Conn as "Connection"
participant Rel as "Relation"
participant Coll as "Collection"
App->>Model : query()/where()/with()...
Model->>Builder : newQuery()
Builder->>Conn : table(...).where(...)
App->>Builder : get()/paginate()
Builder->>Builder : applyGlobalScopes()
Builder->>Conn : select()/paginate()
Conn-->>Builder : rows
Builder->>Model : newFromBuilder(rows)
Builder->>Rel : with eager load (addEagerConstraints/getEager/match)
Builder->>Coll : newCollection(models)
Builder->>PrefetchRunner : run(rows, model)
Coll-->>App : 返回集合
详细组件分析
Model 基类:ActiveRecord 与属性映射
-
设计要点
- 表名、主键、fillable、casts、with、prefetchers、appends、translatable、timestamps 等元信息集中管理。
- 属性访问优先级:已加载关系 → accessor → cast → 原始值 → 惰性关系。
- 属性写入优先级:setXxxAttribute 修改器 → 可逆 cast 写入转换 → 原样写入。
- 脏检测:基于 original 快照对比,cast 字段使用宽松比较避免误报。
- 事件:deleting/deleted 钩子,支持注册监听器并在删除前/后触发。
- 全局作用域:按模型类分桶注册,Builder 在读路径一次性应用。
- 连接解析:通过 setConnectionResolver 注入,默认走 DB 门面根实例。
- 静态入口:__callStatic 转发到 query(),便于 Model::where()/with()/create() 等调用。
- 模板兼容:ArrayAccess/IteratorAggregate/Countable/JsonSerializable,使实例可直接用于模板。
-
关键流程
- 构造:bootIfNotBooted → bootTraits → boot,可选填充 attributes。
- 实例工厂:newQuery/newInstance/newFromBuilder/newCollection。
- 属性读写:getAttribute/setAttribute/getRawAttribute/getDirty/isDirty/syncOriginal。
- 关系访问:getRelationshipFromMethod 缓存至 relations。
- 保存/删除:fill→save(含时间戳)/ create / destroy(触发事件)。
classDiagram
class Model {
+string table
+string primary
+array fillable
+array casts
+array with
+array prefetchers
+array appends
+array translatable
+bool timestamps
+getTable()
+query()
+create(attrs)
+destroy(id)
+setAttribute(key,val)
+getAttribute(key)
+getDirty()
+fireModelEvent(event)
}
Builder 查询构建器:Builder 模式与全局作用域
-
设计要点
- 在底层 Connection 之上叠加 with 预加载、全局作用域应用、结果水合与 prefetch。
- __call 优先查找 scope{Ucfirst(method)} 同名方法,否则透传到 Connection;聚合方法白名单确保 count/sum/exists 等与 get() 行为一致。
- whereKey/orderBy/find/first/get/paginate 等终结方法完成 SQL 执行与结果处理。
- hydrateRows 将行集转为 Model 集合,随后执行 eager load、prefetch、afterHydrate 回调。
- withoutGlobalScope/withoutGlobalScopes 支持局部跳过全局作用域。
-
典型流程(get)
- 应用全局作用域 → 执行 select → 水合为 Model 集合 → eager load → prefetch → 回调 → 返回 Collection。
flowchart TD
Start(["进入 get()"]) --> ApplyGS["应用全局作用域"]
ApplyGS --> Exec["执行底层 select()"]
Exec --> Hydrate["水合为 Model 集合"]
Hydrate --> Eager{"有 with 预加载?"}
Eager -- 是 --> LoadRel["执行 eager load"]
Eager -- 否 --> Prefetch
LoadRel --> Prefetch["运行 PrefetchRunner"]
Prefetch --> After["执行 afterHydrate 回调"]
After --> End(["返回 Collection"])
关系系统:Relation 抽象与四种关系实现
-
抽象契约
- addEagerConstraints:收集父模型键集合用于批量 whereIn。
- getEager:批量查询关联结果。
- match:将结果回填到父模型集合。
- getResults:单父模型的惰性取值。
- setEagerOptions/applyEagerOptions:注入嵌套 with 与约束闭包。
-
关系实现
- HasOne:一对一,foreignKey 指向 localKey。
- HasMany:一对多,foreignKey 指向 localKey。
- BelongsTo:多对一,父模型 foreignKey 指向关联 localKey。
- BelongsToMany:多对多,两步法(pivot → related),避免 JOIN 别名限制,保持 eager/lazy 一致。
classDiagram
class Relation {
+addEagerConstraints(models)
+getEager()
+match(models, results, name)
+getResults()
+setEagerOptions(nested, constraint)
}
class HasOne
class HasMany
class BelongsTo
class BelongsToMany
Relation <|-- HasOne
Relation <|-- HasMany
Relation <|-- BelongsTo
Relation <|-- BelongsToMany
类型转换:CastResolver 读写分离
- 读取方向(apply):int/float/bool/string/json/array/datetime/date/timestamp/attachment/attachment_thumb/defined_pairs/data_lang 等。
- 写入方向(applySet):仅对可逆类型反向转换,展示型 cast(如 attachment、data_lang)原样返回,避免二次转换。
- 时间兼容:datetime/date/timestamp 读取兼容 int 时间戳与 DATETIME 字符串双态,写入统一产出 'Y-m-d H:i:s'。
flowchart TD
In(["属性写入/读取"]) --> Dir{"方向"}
Dir -- 读 --> Read["apply(type, value, model)"]
Dir -- 写 --> Write["applySet(type, value, model)"]
Read --> Types{"类型分支"}
Write --> Types
Types --> |基础类型| Base["int/float/bool/string/json/array"]
Types --> |时间| Time["datetime/date/timestamp"]
Types --> |附件/多语言| Ext["attachment/attachment_thumb/defined_pairs/data_lang"]
Base --> Out(["返回转换后的值"])
Time --> Out
Ext --> Out
预加载:PrefetchRunner 声明式批量预热
- 依据模型 $prefetchers 配置,在批量读取后统一执行:
- url:URL 缓存预热。
- language:语言字段批量 warmup。
- attachment/attachment_thumb:附件 URL 批量生成。
- gallery_first:批量获取首图映射。
- 未知名称静默忽略并记录调试日志,生产无噪声。
flowchart TD
Start(["批量读取结束"]) --> Check{"是否有 prefetchers?"}
Check -- 否 --> End(["结束"])
Check -- 是 --> Run["遍历 prefetchers"]
Run --> Case{"名称匹配"}
Case -- url --> Url["Url::warmupUrlCache"]
Case -- language --> Lang["language()->warmup(table, ids, fields)"]
Case -- attachment* --> Att["attachment()->urlBatch(numbers, thumb?)"]
Case -- gallery_first --> Gal["attachment()->galleryFirstMap(table, ids)"]
Url --> Next["继续下一个"]
Lang --> Next
Att --> Next
Gal --> Next
Next --> End
集合:Collection 结果集处理
- 功能:all/first/last/isEmpty/map/filter/pluck/keyBy/modelKeys/push/toArray/jsonSerialize。
- 用途:对 Model 集合进行迭代、过滤、字段提取、键控重建、JSON 输出等,兼容 Smarty/数组访问。
依赖关系分析
- Model 依赖:
- 连接层:通过 getConnection() 获取 Connection 门面根实例,或注入自定义解析器。
- 关系层:HasOne/HasMany/BelongsTo/BelongsToMany 作为关系方法返回。
- 类型转换:CastResolver 在属性读写时调用。
- 预加载:PrefetchRunner 在 Builder 水合后调用。
- Builder 依赖:
- 底层 Connection:所有链式方法最终透传至 Connection。
- 关系系统:with/eager load 通过 Relation 接口协作。
- 集合:返回 Collection。
- 关系实现依赖:
- 均依赖 Model::newQuery() 与 Connection 的 where/inWhere/select 等能力。
- BelongsToMany 额外依赖中间表 pivot 的两步加载。
graph LR
M["Model"] --> B["Builder"]
M --> R["Relation"]
M --> CR["CastResolver"]
B --> Conn["Connection"]
B --> PR["PrefetchRunner"]
R --> H1["HasOne"]
R --> H2["HasMany"]
R --> BT["BelongsTo"]
R --> BM["BelongsToMany"]
性能与优化
- 预加载(Eager Loading)
- 通过 Builder::with('relation') 或 Model::$with 声明,减少 N+1 查询。
- 支持嵌套 with('a.b.c') 与约束闭包,精准控制关联查询。
- 声明式预加载(Prefetch)
- 通过 $prefetchers 批量预热 URL、语言、附件、首图等,避免在模板渲染阶段逐条计算。
- 懒加载(Lazy Loading)
- 未显式 with 的关系在首次访问时按需加载,适合小范围场景。
- 全局作用域
- 在 Builder 读路径一次性应用,保证 count/sum/exists 等聚合与 get() 行为一致。
- 批量操作
- 使用 whereKey/whereIn 等批量条件,结合 destroy 触发事件的安全删除。
- 类型转换优化
- 写入方向仅对可逆类型转换,展示型 cast 不反向,避免重复计算。
故障排查指南
- 主键参数校验
- find/whereKey 拒绝 null/空串/数组/对象,防止误查或 PHP 8 类型错误。
- 全局作用域不一致
- 聚合方法白名单确保 count/sum/exists 等自动应用全局作用域,避免与 get()->count() 结果不一致。
- 未知 prefetcher
- 未知名称会记录调试日志并忽略,检查拼写或扩展注册。
- JSON 解析失败
- json_decode 失败时记录警告并返回空数组,检查存储格式。
- 时间字段兼容
- datetime/date/timestamp 读取兼容 int 与字符串,写入统一格式化,注意迁移期数据。
结论
DouPHP ORM 以 Model 为核心,配合 Builder 的链式查询与全局作用域、Relation 的统一关系抽象、CastResolver 的类型转换与 PrefetchRunner 的声明式预加载,形成高效、可扩展的数据访问层。其设计兼顾可读性与性能,通过预加载与批量操作有效规避 N+1 问题,同时保留与底层 Connection 的共存能力,满足复杂业务场景下的灵活需求。
附录:常用用法示例路径
- CRUD 操作
- 创建:Model::create([...])
- 更新:$model->fill([...])->save()
- 删除:Model::destroy($id) 或 $model->delete()
- 查询:Model::where(...)->get()/first()/find()
- 参考路径
- Model.php:502-508
- Model.php:519-543
- Builder.php:266-306
- 关联查询
- 一对一:hasOne('relation', 'foreign_key', 'local_key')
- 一对多:hasMany('relation', 'foreign_key', 'local_key')
- 多对一:belongsTo('relation', 'foreign_key', 'local_key')
- 多对多:belongsToMany('relation', 'pivot_table', 'foreign_pivot_key', 'related_pivot_key')
- 参考路径
- HasOne.php:24-90
- HasMany.php:24-88
- BelongsTo.php:24-89
- BelongsToMany.php:24-172
- 批量操作
- 批量删除触发事件:Model::destroy([ids])
- 批量条件:whereKey/whereIn
- 参考路径
- Model.php:519-543
- Builder.php:238-244
- 预加载与懒加载
- 预加载:Model::with(['relation','nested'])->get()
- 懒加载:直接访问 $model->relation
- 参考路径
- Builder.php:175-230
- Relation.php:123-128
- 类型转换
- 声明 casts 字段,自动在读取/写入时转换
- 参考路径
- CastResolver.php:51-138
- CastResolver.php:140-174