Laravel 递归查询:高效排除指定父级及其所有子孙节点

心靈之曲
发布: 2025-12-02 12:54:18
原创
430人浏览过

Laravel 递归查询:高效排除指定父级及其所有子孙节点

本文详细介绍了在 laravel 递归关系中,如何高效地查询并排除指定父级及其所有子孙节点的数据。通过利用 laravel 的模型关系和自定义查询作用域,结合一个辅助的扁平化函数,本教程提供了一种实用的解决方案,用于处理层级数据结构中复杂的排除逻辑,确保精准获取所需数据。

在构建具有层级结构的应用时,例如分类、标签或评论系统,我们经常会遇到需要处理递归关系的情况。Laravel 提供了强大的 Eloquent ORM,通过自关联关系可以很好地管理这类数据。本教程将深入探讨如何在一个递归模型中,实现一个高级查询,即排除指定父级及其所有子孙节点的数据。

理解递归关系模型

首先,我们定义一个名为 Hobbies 的模型,它代表一个具有层级关系的兴趣爱好列表。该模型通过 parent_id 字段指向其父级爱好,从而形成一个树状结构。

// app/Models/Hobbies.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class Hobbies extends Model
{
    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');
    }

    // ... 其他方法和作用域将在此处添加
}
登录后复制

在上述模型中:

  • 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)和辅助方法来完成。

大师兄智慧家政
大师兄智慧家政

58到家打造的AI智能营销工具

大师兄智慧家政 99
查看详情 大师兄智慧家政
// app/Models/Hobbies.php (续)
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class Hobbies extends Model
{
    // ... 之前定义的属性和关系方法

    /**
     * 将嵌套的 Eloquent 集合或数组扁平化为包含所有独立项的数组。
     *
     * @param array $array 待扁平化的嵌套数组。
     * @return array 扁平化后的结果数组。
     */
    private function flatten(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); // 过滤掉嵌套关系和对象
                });
                // 递归处理嵌套关系,并将结果合并
                $result = array_merge($result, $this->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 作用域实现

这个作用域是主要的查询逻辑:

  1. 获取要排除的爱好及其子孙线: Hobbies::with('allsub')-youjiankuohaophpcnwhere('id', $id)->get()->toArray(); 这一步首先通过 with('allsub') 加载指定 ID 的爱好及其所有递归子孙。.get()->toArray() 将 Eloquent 集合转换为 PHP 数组,以便后续的扁平化处理。

  2. 扁平化结果并提取 ID: $flattenedHobbies = collect($this->flatten($hobbiesToExclude));$excludeIds = $flattenedHobbies->map(...)->filter()->unique()->all(); 这里利用 flatten 方法将复杂的嵌套数组结构转换为一个简单的数组,其中每个元素都是一个独立的爱好数据数组。接着,通过 Laravel 的集合方法 map 提取出每个爱好的 id,filter 移除可能存在的空值,unique 确保 ID 不重复,最后 all() 转换为纯 PHP 数组。

  3. 使用 whereNotIn 排除: $query->whereNotIn('id', $excludeIds); 这是 Eloquent 的核心功能,它会修改当前查询,使其只返回 ID 不在 $excludeIds 列表中的爱好。

  4. 额外过滤条件(可选): $query->whereDoesntHave('is_archive'); 原始问题中包含了一个 whereDoesntHave('is_archive') 的条件。这表示在排除指定父子线的同时,还希望排除那些具有 is_archive 关系(例如,表示已归档)的爱好。如果你的模型没有这个关系,或者不需要这个过滤,可以将其移除。

使用方法

一旦 scopeIsNotLine 作用域被定义,你就可以像使用任何其他 Eloquent 作用域一样来使用它:

use App\Models\Hobbies;

// 假设要排除 ID 为 1 的爱好及其所有子孙
$targetId = 1;
$filteredHobbies = Hobbies::isNotLine($targetId)->get();

// $filteredHobbies 将包含所有不属于 ID 为 1 的爱好及其子孙的爱好
foreach ($filteredHobbies as $hobby) {
    echo $hobby->name . "\n";
}
登录后复制

注意事项与优化

  1. 性能考虑

    • 对于非常深或非常宽的递归层级,with('allsub') 会导致大量的 JOIN 操作,可能影响查询性能。
    • 将 Eloquent 集合转换为数组 (toArray()) 并在 PHP 中进行扁平化处理,对于大规模数据集可能会消耗较多内存和 CPU 资源。
    • 如果性能成为瓶颈,可以考虑在数据库层面进行优化,例如使用 递归 CTE (Common Table Expressions)。MySQL 8+、PostgreSQL 和 SQL Server 都支持 CTE,可以在数据库层面直接生成排除列表,从而减少 PHP 端的处理负担。
  2. flatten 方法的健壮性: 当前 flatten 方法假设模型属性是标量或简单的数组。如果模型关系中包含更复杂的对象或集合,可能需要调整 array_filter 的条件。

  3. 替代方案:闭包表或路径枚举: 对于非常复杂的递归关系查询,更专业的解决方案是使用 闭包表 (Closure Table)路径枚举 (Path Enumeration) 模式。这些模式通过额外的辅助表或字段来存储节点间的路径信息,从而将递归查询转换为简单的非递归查询,极大地提高查询效率和灵活性。Laravel 社区也有一些包实现了这些模式。

总结

本教程提供了一种在 Laravel 中处理递归关系并排除特定分支的实用方法。通过结合 Eloquent 的递归关系加载、自定义的扁平化函数和查询作用域,我们能够有效地实现复杂的层级数据过滤需求。尽管这种 PHP 端的处理方式对于中小型数据集非常有效,但在处理大规模或深度层级数据时,建议考虑数据库层面的优化方案以获得更好的性能。

以上就是Laravel 递归查询:高效排除指定父级及其所有子孙节点的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号