关联查询加条件核心是wherehas()筛主模型、with()闭包筛关联数据:wherehas()用exists子查询按关联字段过滤主模型,with()仅精简预加载的关联数据,二者组合可同时实现筛选与避免n+1。

关联查询加条件,核心就两条路:用 whereHas() 筛主模型,用 with() 闭包筛关联数据——选错就查不到想要的结果,或者查出来但 N+1。
whereHas() 是唯一能按关联字段筛选主模型的方法
比如要“找出所有职业是 teacher 的用户”,不能写 User::where('userAttr.job', 'teacher'),这会报错或返回空——Eloquent 不支持跨表字段直接 where。必须用 whereHas():
-
whereHas()第一个参数是关系方法名(如'userAttr'),必须和模型里定义的函数名完全一致(大小写敏感) - 闭包里的
$query操作的是关联表(user_attrs),所以可直接写where('job', 'teacher') - 必须调用
->get()、->first()等执行方法,否则只是构建器对象,没真正查库 - 生成的 SQL 是
EXISTS子查询,性能通常比join更稳,尤其在关联表有索引时
with() 闭包只影响预加载的数据,不改变主模型结果集
如果你写 User::with(['userAttr' => function ($q) { $q->where('job', 'teacher'); }])->get(),它会返回所有用户,但每个用户的 $user->userAttr 只包含 job = 'teacher' 的那条记录(如果存在)。这不是筛选用户,只是“精简了附带查出来的关联数据”。
- 适合场景:列表页展示全部用户,但只显示他们符合条件的属性(比如只加载已审核的订单)
- 错误用法:想靠它来过滤用户列表,结果发现用户一个没少,只是部分
userAttr是 null - 闭包里可以链式调用
orderBy()、limit()等,但注意limit()在一对多时才有效,一对一无效
需要同时筛选 + 预加载?链式组合,顺序无关但推荐 with 在前
常见需求是:“查出所有 job = 'teacher' 的用户,并且把他们的 userAttr 一起带上”。这时要把 with() 和 whereHas() 一起用:
$users = User::with('userAttr')
->whereHas('userAttr', function ($query) {
$query->where('job', 'teacher');
})
->get();
- 两个方法可互换顺序,SQL 效果一样;但把
with()放前面更符合直觉(先说“我要带什么”,再说“我要哪些”) - 这样既避免了 N+1(
userAttr一次性查完),又确保了主模型被正确过滤(whereHas保证每个用户确实有匹配的userAttr) - 访问时可安全用
$user->userAttr->job,不用判空(whereHas已排除无关联记录的用户)
多个关联条件?叠用 whereHas,别塞进一个闭包
比如要查“既有 job = 'teacher',又有 salary > 5000”的用户,可以在同一个 whereHas() 闭包里写两个 where。但若条件来自不同关联(比如还要满足 posts 里有 status = 'published'),就得叠用:
$users = User::whereHas('userAttr', function ($q) {
$q->where('job', 'teacher')->where('salary', '>', 5000);
})
->whereHas('posts', function ($q) {
$q->where('status', 'published');
})
->get();
- 每个
whereHas()对应一个独立 EXISTS 子查询,逻辑是 AND 关系 - 避免把不同关联的条件硬塞进同一个闭包——
whereHas('userAttr', ...)里的$query只能操作user_attrs表,不能碰posts - 如果关联太多、条件太动态,建议封装成查询类或 scope,而不是堆 if-else 在闭包里
最易忽略的一点:关系方法名拼错或大小写不对,whereHas() 会静默失败(返回空集合,不报错)。调试时先确认模型里 public function userAttr() 确实存在,再检查调用时写的字符串是不是完全一致。











