预加载需精准配置,传数组而非链式调用,嵌套关系用点号语法(如'posts.comments'),load()适用于已查模型的按需补查,字段限制、sql验证和模型查询前提缺一不可。

预加载不是“加个 with() 就完事”,用错方式反而让 N+1 更隐蔽、更难排查。
with() 传数组还是链式调用?
传数组是正确姿势,链式调用会触发多次独立查询,且嵌套关系根本不会被加载。
-
User::with('posts', 'comments')->get()→ 主表 + posts 表 + comments 表,共 3 次查询;但comments如果本该属于posts下的子关联(比如Post::comments()),那它压根不会出现——$user->posts->first()->comments仍是 null - 要加载「用户→文章→评论」,必须写成
User::with('posts.comments')->get() - 要同时加载「用户→文章」和「用户→个人资料」,才用并列数组:
User::with(['posts', 'profile'])->get() - 多个嵌套路径混搭也没问题:
StoreStock::with(['product.price', 'product.masterlist'])->get(),Eloquent 会自动按外键分组查,不是暴力 JOIN
什么时候该用 load() 而不是 with()?
load() 是模型实例已存在后补查,with() 是查询前声明。硬套 with() 可能白查、漏查、或破坏条件逻辑。
- 控制器里先查出一批
User,后续根据请求参数决定是否需要加载posts:这时用$users->load('posts'),避免无条件预加载 - 同一个关系多次
load()会重复执行 SQL,得自己判断是否已加载:if (!$user->relationLoaded('posts')) { $user->load('posts'); } -
load()不支持点号嵌套语法(如'posts.comments')——必须先确保posts已加载,再对每个Post实例单独load('comments')
预加载字段太多?小心内存和性能反噬
预加载默认查全部字段,大表或宽表可能拖慢查询、撑爆内存,尤其配合 withCount() 或聚合时。
- 只取必要字段:
with(['posts' => function ($q) { $q->select('id', 'user_id', 'title'); }]),注意必须包含外键(user_id)和主键(id),否则关联失败 -
withCount('posts')和with('posts')是两回事:前者只查数量,后者查整行数据;两者同时存在,Laravel 仍会发两条 SQL - 不要在同一个
withSum()里塞多个条件;多条件统计必须拆成多个带闭包的withCount(),例如:withCount(['posts as drafts_count' => fn($q) => $q->where('status', 'draft')])
怎么确认预加载真的生效了?
别信代码长得像,要看真实 SQL 和内存行为。
- 用
dd($users)看输出里relations是否包含目标关联,且wasRecentlyCreated等非关联字段没被污染 - 开 Laravel Telescope 或直接看日志:
DB::enableQueryLog()+dd(DB::getQueryLog()),确认只有预期的几条 SELECT,没有循环中冒出的额外查询 - 如果用了
select()但没包含外键字段,预加载会静默失败——关联对象存在但内部为空,访问$user->posts返回空集合,不报错也不提示
最常被忽略的一点:预加载只作用于 Eloquent 查询,DB::table() 或原生查询完全不认 with();想优化这类场景,得手动 JOIN 或重构为模型查询。











