TP中with()预加载必须配合select()/paginate()执行,否则仍N+1;字段裁剪需在with闭包内指定含外键的字段;嵌套预加载应统一用数组语法;cursor()与with()组合易致内存暴涨。

with() 后 select() 不执行,照样 N+1
写了 with('profile') 却没调用 select() 或 find(),TP 就不会真正做预加载,而是等你第一次访问 $user->profile 时才发起单独查询——查 100 个用户,就发 100 次 profile 查询。
必须确保 with() 紧跟主查询方法:
-
User::with('profile')->select()✅ -
User::with('profile')->paginate(20)✅($with = ['profile']在分页里不生效,必须显式写) -
User::with('profile')❌(只是构建查询器,没执行) -
User::with('profile')->where('status', 1)->get()❌(TP6 不识别get(),应改用select())
字段裁剪写错位置,等于白干
field() 写在主查询链上,对关联表完全无效。比如 User::with('profile')->field('id,name')->select(),profile 表仍查全部字段,大字段(如 resume_text)会直接拖垮内存和网络。
正确做法是把 field() 放进 with() 的闭包里,并且必须包含外键和主键:
立即学习“PHP免费学习笔记(深入)”;
- ✅
with(['profile' => function($q) { $q->field('id,user_id,nickname,avatar'); }]) - ❌
with('profile')->field('id,nickname')(只影响 User 表) - ❌
with(['profile' => function($q) { $q->field('nickname,avatar'); }])(缺id和user_id,绑定失败,$user->profile为null)
嵌套预加载写法错,静默失效
TP6.0–6.0.12 不支持 with('user.profile') 这种点号写法,会直接忽略后两层;TP6.1+ 虽支持,但要求每层关联方法都存在且返回正确对象,否则整个链路中断、无报错、数据为 null。
稳妥写法统一用数组语法:
- TP6.0–6.0.12:用
with(['user.profile.avatar']) - TP6.1+:推荐
with(['user' => ['profile' => ['avatar']]]),每层可加独立闭包 - 别混用:
with(['user.profile' => ['avatar']])会触发Array to string conversion - 验证是否通路:手动执行
dd(User::find(1)->profile->avatar),能取到才是真生效
大数据量下 with() + cursor() 是个坑
cursor() 看似适合流式读取,但 with('posts') 配合它时,每次迭代仍会为当前用户完整加载所有 posts。一个用户有 5000 篇文章,单次迭代就可能吃掉上百 MB 内存。
更可控的方案:
- 用
paginate()控制单页数量,配合前端滚动加载 - 拆成两步:先查用户 ID 列表(
field('id')),再按批次whereIn('user_id', $ids)查 posts,自己控批次大小 - 避免三层以上嵌套(如 A→B→C→D),TP 不做字段自动别名,容易因同名字段(如多个
id)覆盖导致数据错乱
with() 更容易被忽略——它不报错,但数据为空、性能崩坏、排查无日志。



















