Eloquent 的 with() 预加载可避免 N+1 查询,通过 2 条 SQL 一次性加载关联数据;支持嵌套、字段筛选(需含外键)、正确关联定义及 whereHas/约束 with 区分使用场景,慎用深层嵌套以防笛卡尔积。

用 Eloquent 的 with() 预加载避免 N+1 查询
直接在查询中调用关联属性(比如 $user->posts)会触发懒加载,每查一个模型就额外发起一次 SQL,100 个用户就会执行 100 次 SELECT * FROM posts WHERE user_id = ? —— 这是性能杀手。
正确做法是用 with() 一次性预加载:
$users = User::with('posts')->get();
foreach ($users as $user) {
echo $user->name;
foreach ($user->posts as $post) { // 此处不再查库
echo $post->title;
}
}-
with('posts')底层生成一条 JOIN 或独立的SELECT ... WHERE id IN (...),只发 2 条 SQL - 支持嵌套:例如
with('posts.comments')加载三级关联 - 如果只想要部分字段,用
with(['posts' => function ($q) { $q->select('id', 'title', 'user_id'); }]),但注意必须包含外键字段(如user_id),否则关联会断
关联方法定义要匹配实际外键和本地键
模型里写错 belongsTo() 或 hasMany() 的参数,会导致 with() 查不到数据或报错,但错误不明显。
常见陷阱:
立即学习“PHP免费学习笔记(深入)”;
-
Post::belongsTo(User::class)默认找user_id字段,但如果数据库里叫author_id,就得显式传参:belongsTo(User::class, 'author_id') -
User::hasMany(Post::class)默认按post.user_id = user.id关联;如果Post表主键不是id(比如叫pid),要加第三个参数:hasMany(Post::class, 'user_id', 'pid') - 多对多中间表命名不规范(比如没按
user_post字母序)时,belongsToMany()必须指定表名:belongsToMany(Role::class, 'user_roles')
需要筛选关联数据时,用 whereHas() 或约束 with()
想查「有已发布文章的用户」,不能写 User::with('posts')->where('posts.status', 'published') —— 这会报错,因为 posts 不在 users 表里。
分两种场景处理:
- 只要用户满足关联条件(不取关联数据):用
whereHas()User::whereHas('posts', function ($q) { $q->where('status', 'published'); })->get(); - 既要用户,又要带筛选后的关联数据:约束
with()User::with(['posts' => function ($q) { $q->where('status', 'published'); }])->get(); - 注意:约束
with()不影响主模型数量,而whereHas()会过滤主模型
大数据量下慎用深层嵌套 + 全字段 with()
写 User::with('posts.comments.author') 看似方便,但容易引发三张表笛卡尔积,尤其当某用户有 50 篇文章、每篇有 20 条评论时,内存和查询结果集会爆炸。
- 优先考虑是否真需要全部字段;用
select()限定字段能显著减少数据传输量 - 分页场景下,
with()无法限制关联数据的条数(比如只取每个用户的最新 3 篇文章),此时得换思路:用子查询或应用层聚合 - MySQL 8.0+ 可借助
LATERAL子查询实现“为每行主模型取 Top N 关联”,但 Eloquent 原生不支持,需手写原生 SQL 或用包如laravel-adjacent
关联查询不是越深越好,字段不是越多越全,关键看接口实际要什么数据——漏掉外键、写错约束、盲目嵌套,这三个点最容易在线上突然拖慢接口。











