预加载 with 和筛选 whereHas 作用对象不同:with 控制预加载哪些关联记录,whereHas 控制主模型是否被保留;二者缺一不可,漏掉 whereHas 则主模型不会被筛选。

with 预加载 + whereHas 筛选不生效?先确认执行顺序
预加载 with 和筛选 whereHas 是两件事:前者只管“把关联数据查出来”,后者才真正“过滤主模型”。如果写成 ->with(['posts' => fn ($q) => $q->where('status', 1)])->whereHas('posts', fn ($q) => $q->where('status', 1)),看起来重复,但缺一不可——with 里的闭包控制**预加载哪些关联记录**,whereHas 控制**主模型是否被保留**。
常见错误现象:with 加了条件,但分页结果里主模型没被过滤,或者关联数据为空但主模型还在。
-
whereHas必须显式调用,否则with的条件只影响预加载的关联数据,不影响主模型集合 - 如果只要预加载、不要筛选主模型,就别写
whereHas;反之,漏掉whereHas就等于没筛选 - Laravel 9+ 支持在
with闭包里用select()、orderBy(),但不能替代whereHas的语义作用
分页时 with + whereHas 组合写法(带条件)
直接链式调用即可,Laravel 会自动合并查询。关键点是:分页方法(如 paginate())必须放在最后,且不能插在中间打断逻辑链。
$users = User::with(['posts' => fn ($q) => $q->where('published', true)])
->whereHas('posts', fn ($q) => $q->where('published', true))
->paginate(15);
- 上面代码生成两条 SQL:一条查符合条件的
UserID 列表(含分页偏移),一条用这些 ID 批量查Post - 如果关联表字段需要排序或限制数量(比如每个用户只预加载最新 3 篇文章),得用子查询或
withCount配合后续处理,原生with不支持limit - 注意:闭包里用
fn语法更简洁,但 PHP function ($q)
whereHas 带多条件 or 关联嵌套时容易错在哪
whereHas 默认是 AND 语义,多个 whereHas 调用表示“同时满足多个关联存在”,不是“满足任一”。嵌套关联(如 user.posts.comments)要一层层写,不能跳级。
- 想表达“有已发布文章 OR 有置顶评论”,得用
whereDoesntHave反推,或改用whereRaw+ 子查询,原生不支持 OR 形式的whereHas - 嵌套写法:
whereHas('posts', fn ($q) => $q->whereHas('comments', fn ($c) => $c->where('is_top', true))),少一层闭包就会报Relation not found - 关联名拼错、模型里没定义对应
hasMany/belongsTo方法,都会导致whereHas查不到数据且静默失败(返回空集合)
性能和兼容性要注意的硬伤
这个组合在数据量大时可能触发 N+1 或全表扫描,尤其当 whereHas 条件没走索引,或关联字段类型不匹配(比如用字符串查整数 ID)。
- 确保外键字段(如
post.user_id)和主键类型一致,并建了索引;whereHas中的条件字段也建议加联合索引(如(user_id, published)) - Laravel 8 之前,
with闭包里用orderBy可能导致分页错乱(偏移计算不准),8+ 已修复,但老项目仍需验证 - 如果关联模型用了软删除(
SoftDeletes),whereHas默认不包含deleted_at IS NULL,得手动加上,否则可能筛出已被软删的记录
最常被忽略的是:预加载条件和筛选条件看似一样,但作用对象不同,一个管“查什么关联”,一个管“留哪些主模型”——写混了就白调了。


















