
本文详细介绍了在 laravel 递归关系中,如何高效地查询并排除指定父级及其所有子孙节点的数据。通过利用 laravel 的模型关系和自定义查询作用域,结合一个辅助的扁平化函数,本教程提供了一种实用的解决方案,用于处理层级数据结构中复杂的排除逻辑,确保精准获取所需数据。
在构建具有层级结构的应用时,例如分类、标签或评论系统,我们经常会遇到需要处理递归关系的情况。Laravel 提供了强大的 Eloquent ORM,通过自关联关系可以很好地管理这类数据。本教程将深入探讨如何在一个递归模型中,实现一个高级查询,即排除指定父级及其所有子孙节点的数据。
理解递归关系模型
首先,我们定义一个名为 Hobbies 的模型,它代表一个具有层级关系的兴趣爱好列表。该模型通过 parent_id 字段指向其父级爱好,从而形成一个树状结构。
// app/Models/Hobbies.phphasMany(Hobbies::class, 'parent_id'); } /** * 获取当前爱好的父爱好。 */ public function parent_hobbies() { return $this->belongsTo(Hobbies::class, 'parent_id'); } /** * 递归获取当前爱好的所有子孙爱好。 */ public function allsub() { return $this->sub_hobbies()->with('allsub'); } /** * 递归获取当前爱好的所有祖先爱好。 */ public function allparent() { return $this->parent_hobbies()->with('allparent'); } // ... 其他方法和作用域将在此处添加}
在上述模型中:
sub_hobbies 定义了“一对多”的子级关系。parent_hobbies 定义了“多对一”的父级关系。allsub 和 allparent 是递归关系,它们通过 with(‘allsub’) 和 with(‘allparent’) 实现了加载所有子孙或祖先节点的功能。
核心需求分析
我们的目标是:给定一个特定爱好的 ID,我们希望查询出所有 不属于 该爱好及其任何子孙节点(包括子节点、孙节点等)的爱好。
例如,如果我们的爱好结构如下:
- 爱好 1 - 爱好 11 - 爱好 12 - 爱好 121 - 爱好 122 - 爱好 13- 爱好 2 - 爱好 21 - 爱好 22 - 爱好 221 - 爱好 222 - 爱好 23- 爱好 3 - 爱好 31 - 爱好 32 - 爱好 321 - 爱好 322 - 爱好 33
如果我们提供“爱好 1”的 ID,我们希望得到的结果是除了“爱好 1”、“爱好 11”、“爱好 12”、“爱好 121”、“爱好 122”、“爱好 13”之外的所有爱好。
实现方案:自定义查询作用域
为了实现这一需求,我们将通过在 Hobbies 模型中添加一个自定义查询作用域(Scope)和辅助方法来完成。
// app/Models/Hobbies.php (续)flatten($item)); } } // 过滤掉空的数组项,确保只返回有效的模型数据 return array_filter($result); } /** * 查询所有不属于给定爱好及其子孙线的爱好。 * * @param Builder $query Eloquent 查询构建器实例。 * @param int $id 要排除的父级爱好的ID。 * @return Builder 修改后的查询构建器。 */ public function scopeIsNotLine(Builder $query, int $id): Builder { // 1. 获取指定爱好及其所有子孙爱好 // 使用 with('allsub') 递归加载所有子孙节点 $hobbiesToExclude = Hobbies::with('allsub')->where('id', $id)->get()->toArray(); // 2. 将嵌套结果扁平化,并提取所有需要排除的爱好ID // 调用 flatten 辅助方法将嵌套的爱好数据转换为扁平数组 $flattenedHobbies = collect($this->flatten($hobbiesToExclude)); // 从扁平化后的数据中提取所有爱好的 ID $excludeIds = $flattenedHobbies->map(function ($item) { return $item['id'] ?? null; // 确保 id 存在 })->filter()->unique()->all(); // 过滤空值并去重 // 3. 使用 whereNotIn 条件排除这些 ID $query->whereNotIn('id', $excludeIds); // 4. (可选) 添加其他过滤条件,例如排除已归档的爱好 // 这里的 is_archive 关系是一个假设,你需要根据实际模型定义 // return $query->whereDoesntHave('is_archive'); return $query; // 如果没有 is_archive 关系,直接返回 $query }}
flatten 辅助函数详解
flatten 方法是解决问题的关键之一。由于 with(‘allsub’) 加载的数据是一个嵌套的数组结构(因为关系是递归的),我们需要一个方法来遍历这个嵌套结构,并提取出所有独立的爱好项,无论它们位于哪一层。
它递归地遍历输入的数组。对于每个数组项,如果它本身是一个数组(意味着它可能包含嵌套关系),它会:提取当前项中所有非数组/非对象的值(即当前模型自身的属性,如 id, name, parent_id)。然后递归调用自身处理当前项的子数组(即嵌套关系的数据),并将结果合并到 result 数组中。最终,它返回一个扁平化的数组,其中包含了所有层级的爱好数据。
scopeIsNotLine 作用域实现
这个作用域是主要的查询逻辑:
获取要排除的爱好及其子孙线:Hobbies::with(‘allsub’)->where(‘id’, $id)->get()->toArray();这一步首先通过 with(‘allsub’) 加载指定 ID 的爱好及其所有递归子孙。.get()->toArray() 将 Eloquent 集合转换为 PHP 数组,以便后续的扁平化处理。
扁平化结果并提取 ID:$flattenedHobbies = collect($this->flatten($hobbiesToExclude));$excludeIds = $flattenedHobbies->map(…)->filter()->unique()->all();这里利用 flatten 方法将复杂的嵌套数组结构转换为一个简单的数组,其中每个元素都是一个独立的爱好数据数组。接着,通过 Laravel 的集合方法 map 提取出每个爱好的 id,filter 移除可能存在的空值,unique 确保 ID 不重复,最后 all() 转换为纯 PHP 数组。
使用 whereNotIn 排除:$query->whereNotIn(‘id’, $excludeIds);这是 Eloquent 的核心功能,它会修改当前查询,使其只返回 ID 不在 $excludeIds 列表中的爱好。
额外过滤条件(可选):$query->whereDoesntHave(‘is_archive’);原始问题中包含了一个 whereDoesntHave(‘is_archive’) 的条件。这表示在排除指定父子线的同时,还希望排除那些具有 is_archive 关系(例如,表示已归档)的爱好。如果你的模型没有这个关系,或者不需要这个过滤,可以将其移除。
使用方法
一旦 scopeIsNotLine 作用域被定义,你就可以像使用任何其他 Eloquent 作用域一样来使用它:
use AppModelsHobbies;// 假设要排除 ID 为 1 的爱好及其所有子孙$targetId = 1;$filteredHobbies = Hobbies::isNotLine($targetId)->get();// $filteredHobbies 将包含所有不属于 ID 为 1 的爱好及其子孙的爱好foreach ($filteredHobbies as $hobby) { echo $hobby->name . "n";}
注意事项与优化
性能考虑:
对于非常深或非常宽的递归层级,with(‘allsub’) 会导致大量的 JOIN 操作,可能影响查询性能。将 Eloquent 集合转换为数组 (toArray()) 并在 PHP 中进行扁平化处理,对于大规模数据集可能会消耗较多内存和 CPU 资源。如果性能成为瓶颈,可以考虑在数据库层面进行优化,例如使用 递归 CTE (Common Table Expressions)。MySQL 8+、PostgreSQL 和 SQL Server 都支持 CTE,可以在数据库层面直接生成排除列表,从而减少 PHP 端的处理负担。
flatten 方法的健壮性:当前 flatten 方法假设模型属性是标量或简单的数组。如果模型关系中包含更复杂的对象或集合,可能需要调整 array_filter 的条件。
替代方案:闭包表或路径枚举:对于非常复杂的递归关系查询,更专业的解决方案是使用 闭包表 (Closure Table) 或 路径枚举 (Path Enumeration) 模式。这些模式通过额外的辅助表或字段来存储节点间的路径信息,从而将递归查询转换为简单的非递归查询,极大地提高查询效率和灵活性。Laravel 社区也有一些包实现了这些模式。
总结
本教程提供了一种在 Laravel 中处理递归关系并排除特定分支的实用方法。通过结合 Eloquent 的递归关系加载、自定义的扁平化函数和查询作用域,我们能够有效地实现复杂的层级数据过滤需求。尽管这种 PHP 端的处理方式对于中小型数据集非常有效,但在处理大规模或深度层级数据时,建议考虑数据库层面的优化方案以获得更好的性能。
以上就是Laravel 递归查询:高效排除指定父级及其所有子孙节点的详细内容,更多请关注php中文网其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1339871.html
微信扫一扫
支付宝扫一扫