laravel-nestedset关联文章查询如何校验父级分类发布状态?
原有查询仅通过whereHas('category')校验了文章直接关联的单级分类发布状态,完全没有覆盖嵌套集结构中分类的上级父链路校验,因此会出现上级分类未发布、但直接分类已发布的文章被错误返回的问题。
laravel-nestedset 基于嵌套集(Modified Preorder Tree Traversal)实现树结构,每个分类节点自带lft(左值)、rgt(右值)字段,节点的全链路祖先(含自身)可以通过左右值范围规则一次性匹配,无需递归查库。
前置确认
先确保两个基础配置正确:
App\Models\Category模型已引入Kalnoy\Nestedset\NodeTraittrait(laravel-nestedset 5.x 的基础配置)App\Models\Article模型已正确定义分类关联:public function category() { return $this->belongsTo(\App\Models\Category::class); }
方案1:基于嵌套集左右值的查询(性能最优)
嵌套集结构的核心判定规则:一个节点的所有祖先节点(从顶级到当前节点自身),必然满足 祖先节点.lft <= 当前节点.lft 且 祖先节点.rgt >= 当前节点.rgt。
基于这个规则写的查询,单条SQL就能完成全链路校验,没有多层子查询嵌套,数据量大时性能最优:
use App\Models\Article; $validArticles = Article::whereExists(function ($query) { // 关联匹配文章直接所属的分类 $query->from('categories as direct_category') ->whereColumn('direct_category.id', 'articles.category_id'); }) ->whereNotExists(function ($query) { // 校验:该分类全链路(含自身)不存在未发布的分类 $query->from('categories as ancestor_category') ->whereColumn('ancestor_category.lft', '<=', 'direct_category.lft') ->whereColumn('ancestor_category.rgt', '>=', 'direct_category.rgt') ->where('ancestor_category.publish', false); }) ->get();
对应你举的测试场景验证:
结构为
news > politics,news.publish=0、politics.publish=1
此时归属politics的文章,关联的direct_category会匹配到politics节点,对应的ancestor_category查询会命中上级news的未发布记录,直接被过滤,符合预期无结果返回。
方案2:基于模型关联的链式写法(可读性更高)
laravel-nestedset 的NodeTrait 自带ancestors关联(返回当前节点的所有上级祖先节点,不包含节点自身),可以直接通过关联校验实现需求,代码可读性更强:
use App\Models\Article; $validArticles = Article::whereHas('category', function ($query) { // 校验直接所属分类自身是发布状态 $query->where('publish', true); // 校验所有上级祖先节点,不存在未发布的分类 $query->whereDoesntHave('ancestors', function ($subQuery) { $subQuery->where('publish', false); }); })->get();
这种实现的逻辑和方案1完全一致,缺点是生成的SQL嵌套层级比方案1稍多,适合分类树层级不深、数据量不大的场景。
- 你的articles表
category_id允许为null,如果需要保留无分类的文章,去掉查询里的whereExists关联直接分类的逻辑即可;如果需要过滤无分类的文章,保留现有逻辑即可 - 如果之前自定义过laravel-nestedset的默认左右值字段名,记得把代码里的
lft、rgt替换成实际使用的字段名 - 两种实现都不会出现N+1查询问题,不需要循环递归查询父级分类ID
内容的提问来源于stack exchange,提问作者user2726957

