4.3 关联关系


文档摘要

4.3 关联关系 模型能单打独斗了,本节让模型牵手:一对一、一对多、多对多怎么定义、怎么用、怎么避免 N+1 查询这个经典性能陷阱。关联是 ORM 提供的最大抽象红利——用对象图代替手写 JOIN,业务代码的可读性完全不同。读完本节,你应当能把书店项目全部的表关系声明清楚,并能一眼认出代码里的 N+1 现象。 一、三种关系:先画图再写代码 书店项目的核心关系固定为三种形态:用户对书籍是一对多(一个卖家挂多本书);书籍对详情是一对一(每本书一份扩展描述);订单对书籍是多对多(一单含多本,一本书出现在多单,中间靠订单明细表连接)。

4.3 关联关系

模型能单打独斗了,本节让模型牵手:一对一、一对多、多对多怎么定义、怎么用、怎么避免 N+1 查询这个经典性能陷阱。关联是 ORM 提供的最大抽象红利——用对象图代替手写 JOIN,业务代码的可读性完全不同。读完本节,你应当能把书店项目全部的表关系声明清楚,并能一眼认出代码里的 N+1 现象。

一、三种关系:先画图再写代码

书店项目的核心关系固定为三种形态:用户对书籍是一对多(一个卖家挂多本书);书籍对详情是一对一(每本书一份扩展描述);订单对书籍是多对多(一单含多本,一本书出现在多单,中间靠订单明细表连接)。定义都写在模型方法里:

// app/model/User.php:一对多 hasMany class User extends Model { public function books() { return $this->hasMany(Book::class, 'seller_id', 'id'); } } // app/model/Book.php:一对一 hasOne + 反向 belongsTo class Book extends Model { public function detail() { return $this->hasOne(BookDetail::class, 'book_id', 'id'); } public function seller() { return $this->belongsTo(User::class, 'seller_id', 'id'); } } // app/model/Order.php:多对多 belongsToMany(经明细表) class Order extends Model { public function books() { return $this->belongsToMany(Book::class, 'order_item', 'book_id', 'order_id'); } }

三个方法的参数记忆法:第二个参数都是「外键在对面表还是本表」的问题——hasMany 的外键在子表、belongsTo 的外键在本表、belongsToMany 的两个外键都在中间表。拿不准时回表结构数外键,永远比背参数顺序可靠。

图 4-2:书店项目 · 关系图谱

图 4-2:书店项目 · 关系图谱

二、用关联:从对象图拿数据

定义之后,关联像属性一样自然取用:

// 一对多:卖家与他的书 $user = User::find(7); $books = $user->books; // Book 模型集合 foreach ($books as $book) { echo $book->title; } // 反向:书找到卖家 $book = Book::find(42); echo $book->seller->nickname; // 一对一:书与详情 echo $book->detail->summary; // 多对多:订单与书 $order = Order::find(1001); foreach ($order->books as $b) { echo $b->pivot->num; // pivot 是中间表行,数量在明细里 }

三、N+1 现场解剖:关联的性能账单

关联直接取用有一个隐性成本,就是臭名昭著的 N+1:

// 危险写法:取 20 本书,循环里逐本查卖家 $list = Book::limit(20)->select(); foreach ($list as $book) { echo $book->seller->nickname; // 每本书触发一条 SELECT user } // 实际 SQL:1 条查书 + 20 条查用户 = 21 条

列表页、接口集合输出几乎必然踩到它。解法是预载入——一条 JOIN 或 IN 把关联一并取回:

// 修正:with 预载入,全程 2 条 SQL $list = Book::with(['seller'])->limit(20)->select(); foreach ($list as $book) { echo $book->seller->nickname; // 不再发新查询 } // 嵌套与条件化预载入 $orders = Order::with(['books' => function ($q) { $q->where('status', 1)->with(['detail']); }])->limit(10)->select();

用 2.2 装的「速度探针」观察改造前后的查询条数,反差非常直观。团队可以把「集合循环里取关联」列入代码评审红线——出现即要求补 with。

四、动手练习:给订单列表页预载入

背景:订单列表要展示每单的书名与卖家昵称,当前页面查询数随订单数线性增长。操作:先开 trigger_sql 记录现状查询条数;把关联声明补齐(Order belongsToMany books、Book belongsTo seller);用嵌套 with 重写查询;对比改造前后 SQL 条数与页面耗时。结果示例:改造前几十条查询,改造后三条左右,页面耗时降到零头。解读:注意嵌套预载入里 where 的位置——条件加在子查询上而非主查询上,位置错了语义完全不同。变式:给「我卖出的书」页面反向做一遍,体会 belongsTo 与 hasMany 在同一份表结构上的两个视角。

⚠️ 常见坑:预载入不是越多越好——with 把关联全量取回,列表页明明只展示昵称却把用户整行拖回来是浪费;大表关联配合 field 限定字段再预载入。

💡 关键直觉:关联的价值在「用对象图表达查询意图」,性能账单要靠预载入自己付——ORM 不会替你省 SQL,只会替你写 SQL。

四、写路径上的关联:一起创建与外键维护

读路径的预载入之外,关联在写路径上还有一个高频现场:一次业务动作要同时落两张关联表——下单要建订单再建明细,发帖要建帖子再同步标签绑定。用关联的写法能把两步收进模型层:订单模型的关联里追加明细,外键由框架按关联定义自动带上,调用方只见「给这个订单加两条明细」一句话。要点有两条:外键字段交给关联定义维护,调用方不要手工填外键(填错即脏数据);成组的关联写入与 4.4 的事务天然相邻——两张表的写入要同成败,包在同一事务里再动手。

本节要点回顾

  • 先画图再声明:三对关系对应 hasMany、hasOne 与 belongsTo、belongsToMany 三组方法,外键位置决定声明方式。
  • 关联即属性:定义后按属性取用,多对多的中间表数据在 pivot 上。
  • N+1 是线性账单:集合循环取关联时查询数随行数增长,列表页必踩。
  • with 是解药:预载入把查询压到常数条,支持嵌套与条件,评审里对循环取关联零容忍。
  • 预载入有成本:with 全量取回关联数据,按展示需要配合 field 精简。

对象网织好了,还差并发秩序:多个用户同时改同一行时,谁来维持一致?下一节事务与锁。


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