Eloquent预加载需正确使用with()避免N+1,必须在get()前调用、保留外键字段、嵌套用闭包而非链式语法,并善用withCount()等聚合方法。

Eloquent 预加载不是“加个 with() 就完事”,没写对反而让 N+1 更隐蔽、更难排查。
什么时候必须用 with() 而不是直接点关系属性
当你在循环中访问模型关联(比如 $user->posts)且未预加载时,Eloquent 每次都会发起新查询——100 个用户 + 每人 5 篇文章 = 501 次查询(1 次查用户 + 100×5 次查文章)。而用 with('posts') 后,最多 2 次查询。
- 典型错误场景:
foreach ($users as $user) { echo $user->posts->count(); }—— 这里触发了 100 次懒加载 - 正确做法:先
$users = User::with('posts')->get();,再循环 - 注意:
with()必须在get()/first()等执行查询前调用,链式调用顺序不能错
嵌套预加载的写法和常见陷阱
要查用户 + 文章 + 文章评论 + 评论作者,不能写成 with('posts', 'posts.comments', 'posts.comments.author') —— 这会生成冗余 JOIN 和重复数据,且 Eloquent 不保证嵌套层级的 SQL 效率。
- 推荐写法:
with(['posts.comments' => function ($q) { $q->with('author'); }]) - 错误写法:
with('posts.comments.author')—— Laravel 9+ 已弃用该语法,会报Call to undefined relationship - 如果嵌套关系里有 where 条件(比如只查已发布的文章),必须用闭包限定:
with(['posts' => fn($q) => $q->where('published', true)])
select() 和 with() 一起用时字段缺失问题
很多人在 select() 里漏掉外键或主键,导致预加载失败或返回空集合。Eloquent 依赖外键字段做数据匹配,不是靠 JOIN。
- 例如:
User::select('id', 'name')->with('posts')->get()是安全的(保留了id) - 但:
User::select('name')->with('posts')->get()会导致所有$user->posts为空 —— 因为没选id,Eloquent 无法把 posts 关联回来 - 同理,关联表的外键也得显式选上:
Post::select('id', 'title', 'user_id')->with('user')->get()
withCount()、withSum() 这类聚合预加载怎么避免 N+1
它们本质是子查询,不拉取完整关联数据,适合只需要数量、求和等统计值的场景,性能比 with() 更轻量。
-
User::withCount('posts')->get()→ 自动添加posts_count属性,1 次查询搞定 User::withCount(['posts as published_posts_count' => fn($q) => $q->where('published', true)])->get()- 注意:
withCount()不会加载关联模型本身,所以不能接着调$user->posts->first()—— 会再次触发查询 - 如果既要数量又要部分数据,得组合使用:
withCount('posts')->with(['posts' => fn($q) => $q->limit(3)])
真正卡住人的往往不是语法,而是预加载后发现数据变少了、字段空了、或者明明写了 with 却还是慢——八成是漏了外键字段、嵌套写法过时、或者在 collection 上又调了未预加载的关系。调试时打开 DB::enableQueryLog() 看实际发了几条 SQL,比猜快得多。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











