5.3 关系模型:多工种协同


5.3 关系模型:多工种协同

本节摘要:关系声明是 Eloquent 的承重结构:belongsTo、hasMany、belongsToMany、morphTo 四种关系覆盖了绝大多数表间协同。本节用工单系统的真实表结构把四种关系全部声明一遍,讲清外键方向、中间表字段、多态关联的场景,以及关系名与查询方法的衔接。读完你能看着表结构图,直接把关系方法写出来。

从"join 写在哪"说起

单表操作上一节已经顺了,真实业务的数据从来不住在一张表里:工单属于队组、单子派给多个工人、附件能挂在工单也能挂在日志上。这些协同若靠到处手写 join,业务代码会迅速变成 SQL 拼图。关系声明把这些协同固化成模型方法:$order->team 一调即得,join 的细节收进框架。

四种关系,一张表结构全覆盖

回看 5.1 节的地基:teams、work_orders(带 team_id 外键)、workers、work_order_worker 中间表。四种关系正好各就各位:

<?php // 一对多(反向):工单属于队组 —— 外键 team_id 在 work_orders 表上 public function team(): BelongsTo { return $this->belongsTo(Team::class); } // 多对多:工单与工人 —— 中间表 work_order_worker 撮合 public function workers(): BelongsToMany { return $this->belongsToMany(Worker::class, 'work_order_worker') ->withPivot('role') // 中间表的自有字段要显式带出 ->withTimestamps(); }
<?php // 队组侧:一对多(正向)—— 一个队组有多个工单 public function orders(): HasMany { return $this->hasMany(WorkOrder::class); } // 多态一对多:附件可以挂在任何业务对象上 // 附件表 attachments 里有 attachable_type 与 attachable_id 两列 public function attachable(): MorphTo { return $this->morphTo(); }

方向判断的口诀只有一句:外键在谁的表里,谁就是"属于"的那一方。工单表里有 team_id,所以工单 belongsTo 队组、队组 hasMany 工单;中间表两头各有外键,所以是 belongsToMany,两边都要声明。多态关联则是把"属于谁"这件事拆成两列:类型列存模型类名、编号列存主键值,一张附件表服务所有业务对象。

关系的日常操作:读、挂、拆

<?php // 读:动态属性拿关联,拿不到或没加载要小心(下一节讲预加载) $team = $order->team; $crew = $order->workers; // 集合 $role = $order->workers->first()?->pivot->role; // 中间表字段从 pivot 取 // 写:多对多的挂载与摘除 $order->workers()->attach($workerId, ['role' => '电工']); // 挂上新工人并注明工种 $order->workers()->sync([1 => ['role' => '工头'], 2 => ['role' => '电工']]); // 全量同步 $order->workers()->detach($workerId); // 摘除 // 一对多的写:从父方造子方,外键自动带上 $team->orders()->create(['title' => '新单子', 'budget' => 50000]); // 存在才建、不存在就更新: BELONGSTOMANY 的常用变体 $order->workers()->syncWithoutDetaching([$workerId => ['role' => '水电']]);

关系方法另一个隐藏用途是当查询入口用:$order->workers() 带括号得到的是关系查询器,可以继续链式加条件——wherePivot、whereHas 都是这么接上的:

<?php // 有电工在场的单子:跨关系做存在性筛选 $orders = WorkOrder::query() ->whereHas('workers', fn ($q) => $q->where('role', '电工')) ->get(); // 只查本队组的三张单:关系名即查询入口 $recent = $team->orders()->latest()->limit(3)->get();

图 5-3:四种关系的协同布局图

图 5-3:四种关系的协同布局图

中间表的进阶用法与常见坑

中间表不只是撮合,还承载关系自身的属性(工种、派工时间),所以 withPivot 必须显式声明要带出的字段;要在中间表上加时间戳就用 withTimestamps。更重的中间表(比如派工要审批流)建议直接为它建独立模型,走一对多的两层关系,别硬塞进 belongsToMany。

⚠️ 常见坑清单:动态属性($order->workers)在不预加载时每次访问都触发查询,循环里这么写就是 N+1 现场,5.4 节的预加载是解药;sync 是全量覆盖语义,只想增量追加用 attach 或 syncWithoutDetaching,用错会静默删光既有关联;多态关联的类型列存的是类名,模型改命名空间会让旧数据失配,迁移时必须连数据一起改。

关系的两个高频追问

关系方法名可以随便起吗?
方法名是项目内的"通用语",建议与业务语义对齐而不是机械复述表关系:Order::crew() 比 workers() 更贴合工地语境,且这个名字会出现在 whereHas、with 的每一处调用里。改名的成本随项目增长线性上升,起名时多想一分钟。

关联数据太多怎么办(一个单子上百个工人)?
关系查询器支持继续链式约束:$order->workers()->limit(10)->get() 按需取,或预加载时加条件只装最近的。把上百条关联整体加载进内存再在 PHP 里截取,是最常见的内存与性能双重浪费。

本节要点回顾

  • 方向口诀:外键在谁表里谁 belongsTo,反向 hasMany,两头外键 belongsToMany,两列定位 morphTo。
  • 关系即入口:方法带括号就是关系查询器,whereHas 与链式条件接得上。
  • 中间表:withPivot 带字段、withTimestamps 记时间,重逻辑中间表升级为独立模型。
  • sync 语义:全量覆盖,增量挂载用 attach,这一字之差会静默删数据。

关系声明完了,但协同的隐患(N+1)和写库的原子性还没处理。下一节收尾数据层:预加载、访问器、事务与批量浇筑。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U