
本教程详细介绍了如何在 laravel 递归关系中,高效地查询并排除指定节点及其所有子孙节点的数据。通过定义 eloquent 模型中的递归关系,并结合自定义的 scope 方法和辅助函数,我们能够从复杂的层次结构数据中,精确地过滤掉特定分支,实现灵活的数据检索。文章涵盖了模型设置、核心逻辑实现、代码示例及性能优化考量。
Laravel 递归关系模型设置
在处理具有父子关系的层级数据时,Laravel Eloquent 提供了强大的递归关系定义能力。假设我们有一个 hobbies 表,其结构如下:
- id- name- parent_id登录后复制
其中 parent_id 字段指向其父级爱好。为了在 Eloquent 模型中表示这种递归关系,我们需要在 Hobbies 模型中定义相应的关联方法:
// app/Models/Hobbies.php<?phpnamespace App\Models;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;class Hobbies extends Model{ use HasFactory; protected $fillable = ['name', 'parent_id']; public function sub_hobbies() { return $this->hasMany(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'); } // ... 其他方法或 Scope}登录后复制上述模型定义中,sub_hobbies 和 parent_hobbies 定义了直接的父子关系。allsub 和 allparent 方法通过 with 语句递归地加载所有子孙或祖先,这对于处理深度不确定的层级结构至关重要。
问题场景:排除特定分支及其所有后代
我们的目标是:给定一个爱好ID,查询所有爱好,但排除该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”。
腾讯Effidit 腾讯AI Lab开发的AI写作助手,提升写作者的写作效率和创作体验
65 查看详情
解决方案实现
为了实现上述目标,我们可以在 Hobbies 模型中添加一个局部作用域(Scope)方法 scopeIsNotLine 和一个私有辅助函数 flatten。
核心思路
获取排除列表: 首先,根据给定的ID,使用 allsub 关系递归地获取该爱好及其所有子孙爱好。扁平化数据: 将获取到的嵌套结果转换成一个包含所有相关爱好ID的扁平数组。执行查询: 使用 whereNotIn 条件,从所有爱好中排除这些ID。代码实现
在 app/Models/Hobbies.php 模型中添加以下方法:
// app/Models/Hobbies.phpclass Hobbies extends Model{ // ... 其他已定义的方法 public function scopeIsNotLine($query, $id) { // 1. 获取要排除的根爱好及其所有子孙爱好 // toArray() 将 Eloquent 集合转换为 PHP 数组,便于后续处理 $hobbiesToExclude = Hobbies::with('allsub')->where('id', $id)->get()->toArray(); // 2. 将嵌套的爱好数据扁平化,提取所有爱好节点的ID // 使用 collect 辅助函数和 map 闭包来提取ID $excludeIds = collect($this->flattenRecursiveData($hobbiesToExclude)) ->map(function ($item) { // 确保 item 是数组且包含 'id' 键 return is_array($item) && isset($item['id']) ? $item['id'] : null; }) ->filter() // 过滤掉 null 值 ->flatten() // 确保结果是扁平数组 ->unique() // 确保ID唯一 ->all(); // 3. 执行查询:排除在 $excludeIds 列表中的所有爱好 // 示例中还包含一个 whereDoesntHave('is_archive') 条件, // 这表示排除那些没有关联 'is_archive' 关系的爱好, // 这是一个额外的业务逻辑,可根据实际需求移除或修改。 return $query->whereNotIn('id', $excludeIds)->whereDoesntHave('is_archive'); } private function flattenRecursiveData(array $array): array { $result = []; foreach ($array as $item) { if (is_array($item)) { // 提取当前项的非数组属性(即当前节点自身的属性,不包含嵌套关系) $result[] = array_filter($item, function ($value) { return !is_array($value) && !is_object($value); }); // 递归处理当前项中的所有嵌套数组(例如 'sub_hobbies') foreach ($item as $key => $value) { if (is_array($value)) { $result = array_merge($result, $this->flattenRecursiveData($value)); } } } } // 过滤掉可能产生的空数组 return array_filter($result); }}登录后复制使用示例
在控制器或任何需要查询的地方,你可以像这样使用 isNotLine 局部作用域:
use App\Models\Hobbies;// 假设要排除的爱好ID是 1$hobbies = Hobbies::isNotLine(1)->get();// $hobbies 集合中将包含除了 ID 为 1 及其所有子孙爱好之外的所有爱好。登录后复制
注意事项与优化
flattenRecursiveData 辅助函数: 这个函数负责将 Laravel with 预加载出来的嵌套数组结构扁平化。它的工作原理是遍历每一个层级的节点,提取其自身的标量属性,并递归地处理其包含的子数组(例如 sub_hobbies 关系)。最终,collect(...)->map(...)->flatten()->unique()->all() 链式操作将这些扁平化的节点转换为唯一的ID列表。性能考量:N+1 问题: Hobbies::with('allsub') 语句本身会通过预加载解决 N+1 问题,但对于非常深的递归层级和大量数据,一次性加载整个分支到内存中可能会消耗较多资源。数据库效率: 对于支持 CTE(Common Table expressions,如 MySQL 8+, PostgreSQL, SQL Server)的数据库,使用 CTE 可以更高效地在数据库层面进行递归查询和过滤,减少应用层的数据处理负担。例如,可以使用 CTE 递归地找出所有要排除的ID,然后直接在主查询中使用 NOT IN。通用性: scopeIsNotLine 中的 whereDoesntHave('is_archive') 是一个额外的条件,用于排除那些没有 is_archive 关系的爱好。如果你的应用没有这个需求,可以将其移除。替代方案:CTE (Common Table expressions): 对于大型或深度递归的数据集,考虑使用数据库的 CTE 功能。你可以在 Laravel 中通过 DB::raw 或编写更复杂的 Eloquent 查询来实现。预排序遍历树 (Nested Set Model) 或路径枚举 (Path Enumeration): 如果层级结构非常深且查询频繁,可以考虑在数据库层面采用这些专门的树结构存储方案,它们能极大地优化树形结构查询的性能。总结
通过在 Laravel Eloquent 模型中定义递归关系,并结合自定义的局部作用域和辅助函数,我们可以有效地处理复杂的层级数据查询需求,例如排除特定分支及其所有子孙节点。这种方法保持了代码的清晰性和 Eloquent 的优雅
以上就是Laravel 递归模型:实现排除特定祖先及其所有后代记录的查询的详细内容,更多请关注php中文网其它相关文章!
