with()中limit()作用于整个子查询而非每父模型;whereHas()内只能查关联表字段;addSelect()子查询需手动处理NULL和类型;JOIN子查询必须显式别名+完整GROUP BY。

直接在 with() 闭包里写 limit() 不会按每个父模型限制子记录数,它作用于整个子查询结果集——这是最常被当成“能用”却实际失效的操作。
whereHas() 闭包里只能查关联表字段
想查「职业是 teacher 且 salary 不为空」的用户,userAttr 是 User 的 hasOne 关联:
-
whereHas('userAttr', function ($q) { $q->where('job', 'teacher')->whereNotNull('salary'); })✅ 正确:所有条件字段都属于user_attrs表 -
whereHas('userAttr', function ($q) { $q->where('users.name', 'like', '%John%'); })❌ 错误:users.name不在关联表中,SQL 报错或返回空 - 主表条件(如
status、name)必须写在whereHas()外层,不能塞进闭包
with() 预加载时加 limit() 会全局生效,不是每父级生效
下面这段代码本意是“每个用户只加载 2 条活跃订单”,但实际效果是:整个查询最多返回 2 条订单记录,可能全属于同一个用户:
$users = User::with(['orders' => function ($query) {
$query->where('status', 'active')->limit(2);
}])->get();
- 真正需要“每用户 2 条”时,必须用
staudenmeir/eloquent-eager-limit扩展包 - 安装后,在
User和Order模型中都 use\Staudenmeir\EloquentEagerLimit\HasEagerLimit - 然后才能安全写:
with(['orders' => fn ($q) => $q->where('status', 'active')->limit(2)]) - 不引入扩展包而硬写
limit(),等于默认放弃业务准确性
子查询字段用 addSelect() 时 NULL 和类型必须手动处理
给用户加一个「最新订单时间」字段:
$users = User::select('users.*')
->addSelect(['last_order_at' => Order::select('created_at')
->whereColumn('user_id', 'users.id')
->orderByDesc('created_at')
->limit(1)
])
->get();
- 某用户无订单时,
last_order_at是NULL,前端访问$user->last_order_at->format(...)会报错 -
last_order_at是字符串,不是Carbon实例,不能直接调用diffForHumans() - 修复方式:加
COALESCE或后端判空,例如用asDateTime()转换:$user->last_order_at ? Carbon::parse($user->last_order_at) : null -
whereColumn()参数必须是字符串(如'users.id'),不能传变量(如$user->id),否则变成固定值查询
JOIN 子查询必须显式别名 + GROUP BY 完整字段
当要用聚合结果参与 WHERE 或 ORDER BY(比如查订单总额 > 500 的用户并按总额倒序),就得用 joinSub() 替代 withCount():
$users = User::select('users.*', 'order_stats.total')
->joinSub(
Order::select('user_id', \DB::raw('SUM(amount) as total'))
->groupBy('user_id'),
'order_stats',
'users.id',
'=',
'order_stats.user_id'
)
->where('order_stats.total', '>', 500)
->orderByDesc('order_stats.total')
->get();
-
joinSub()第二个参数是别名,漏掉或重复会导致 SQL 报错 -
GROUP BY必须包含所有非聚合字段,MySQL 8.0+ 严格模式下不写就直接失败 - 多表 JOIN 时,
id、name这类通用字段必须加前缀,否则 PHP 取值时会被覆盖 - 闭包里写条件、字段名拼写、别名和
GROUP BY—— 四者缺一不可,漏一个就查不到数据或结果错乱


















