本文围绕 Eloquent 关系查询的常见陷阱(N+1)展开,通过 `with`/`load` 预加载与约束预加载解决问题,并用 SQL 观察与简单基准确认效果。
## 模型与数据(可复现)
// App\Models\User.php
class User extends Model {
public function posts() { return $this->hasMany(Post::class); }
}
// App\Models\Post.php
class Post extends Model {
public function user() { return $this->belongsTo(User::class); }
}
示例数据:创建 100 个用户,每个用户 20 篇文章。
## N+1 问题与 SQL 观察
// 反例:遍历中隐式加载关系,导致 N+1
$posts = Post::latest()->take(100)->get();
foreach ($posts as $p) {
echo $p->user->name; // 触发 100 次额外查询
}
验证:启用查询日志或使用 `DB::listen`/`laravel-debugbar` 观察 SQL 次数。
## 预加载(eager loading)
// 正确:一次性加载关联
$posts = Post::with('user')->latest()->take(100)->get();
foreach ($posts as $p) {
echo $p->user->name; // 无额外查询
}
约束预加载:
$posts = Post::with(['user' => function($q) {
$q->select('id','name');
}])->latest()->take(100)->get();
## 分页与批量处理建议
- 使用分页/游标分页,避免一次加载过多记录:`Post::with('user')->cursorPaginate(100)`。
- 选择必要字段,减少传输与内存占用。
## 简单基准(可复现)
对比两种写法的查询次数与耗时:
- 反例:约 101 次查询(1 次列表 + 100 次关联)。
- 预加载:约 2 次查询(列表 + 关联)。
通过 `laravel-debugbar` 或 `DB::listen` 输出确认。
## 注意事项
- `load()` 适合已获取集合后追加预加载;`with()` 适合查询阶段预加载。
- 多层关联用嵌套 `with('posts.comments')`,谨慎控制数据量。
- 避免在 Blade 中意外触发懒加载,尽量在控制器/服务层完成预加载。
## 结语
N+1 是关系 ORM 的常见陷阱。以 `with/load` 预加载结合约束选择字段与分页策略,能在生产中稳定降低查询次数并提升整体吞吐。

发表评论 取消回复