文档目录
ORM系统核心

简介

本文件为 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
添加日期:2026-10-05