
直接用 whereHas 加闭包是最稳妥的路径查询方式,不是所有“父子”都适合用 join,尤其当路径跨三层以上、或需保留空父级时,join 会意外丢数据。
whereHas 闭包里嵌套 whereHas 实现多层路径过滤
比如要查「分类 → 子分类 → 商品」三层中,商品名称含 "laptop" 的完整路径,且只返回有匹配商品的分类和子分类:
-
whereHas('subcategories')先筛出至少有一个子分类的分类 - 闭包内再套
whereHas('products'),并在其闭包里写$query->where('name', 'like', '%laptop%') - 这样既避免了全量加载后 PHP 过滤的性能损耗,又保证路径上每一层都“非空”
- 注意:外层
whereHas不会自动加载关联数据,如需返回完整结构,得额外加with(['subcategories.products'])并对其也加约束闭包
join 容易漏掉无子记录的父级,但能选子表字段
当你需要在结果里直接显示子表字段(比如 posts_tags.tag_id),或做 GROUP BY 统计时,join 是唯一选择。但它有个硬伤:
-
Post::join('posts_tags', 'posts.id', '=', 'posts_tags.post_id')→where(...)会把没打标签的Post全部排除 - 如果业务要求“即使没子记录也要返回父级”,就得改用
leftJoin,但此时where条件若写在子表字段上(如posts_tags.tag_id = 5),会再次变相变成内连接效果 - 正确做法是把子表筛选条件挪到
on子句里:leftJoin('posts_tags', function ($join) { $join->on('posts.id', '=', 'posts_tags.post_id')->where('posts_tags.tag_id', 5); })
模型关系命名不一致会导致 whereHas 失效
whereHas 的第一个参数必须严格匹配模型里定义的关系方法名,大小写、下划线、驼峰都不能错:
- 如果模型里写的是
public function PostTags(),那调用时必须是whereHas('PostTags'),写成'postTags'或'post_tags'都会静默失败(返回空集合) - Laravel 不会报错,也不会触发 SQL 查询,容易误以为数据不存在
- 推荐统一用小驼峰(如
postTags)并在文档或注释里显式标注,避免控制器里反复试错 - 检查方法是否存在最简单的方式是:在 Tinker 里执行
(new Post)->postTags,看是否返回HasMany实例
预加载 with() 里加条件必须用闭包,且不能省略关系名
想查最新一条父记录并同时加载它全部子记录(按 ID 倒序),不能只写 with('children'):
- 正确写法是
with(['children' => fn($q) => $q->orderBy('id', 'desc')]),闭包里才能加排序或 where - 如果漏掉闭包、直接写
with(['children' => ['orderBy' => 'id desc']]),Laravel 会忽略这个数组,按默认顺序加载 - 更隐蔽的坑:
with('children.orderByDesc')这种链式写法根本不存在,属于无效字符串,不会报错但也不生效 - 子查询条件和排序必须在闭包里写死,不能动态传参——如需变量,得用
use ($tagId)捕获
真正难的不是写对一行 whereHas,而是搞清你到底要“路径存在性判断”还是“路径数据提取”:前者用 whereHas,后者才考虑 join 或带约束的 with;混用时,优先保证 SQL 层语义清晰,别为了少写一行 PHP 而让数据库多扫十万行。


















