当前位置:首页 > 文章列表 > 文章 > php教程 > Laravel递归查询排除节点技巧

Laravel递归查询排除节点技巧

2025-11-30 17:45:39 0浏览 收藏

在 Laravel 中处理递归数据结构,如分类或评论回复,经常需要排除指定节点及其所有子孙。本文详细介绍了如何在 Laravel Eloquent 模型中,通过自定义查询作用域和辅助方法,实现高效的递归查询排除功能。以“爱好”模型为例,定义了递归关联关系,并创建了 `scopeIsNotLine` 作用域,该作用域利用 `with('allsub')` 递归加载所有子孙节点,并通过 `flatten` 辅助方法提取需要排除的 ID 列表,最终使用 `whereNotIn` 方法从查询结果中排除这些节点。此方案提供了一种可复用的解决方案,帮助开发者在复杂的层级数据结构中精准筛选数据,提升 Laravel 应用的性能和可维护性。针对深度递归可能存在的性能问题,还提出了使用 CTE (Common Table Expressions) 的替代方案,以优化查询效率。

Laravel 递归关系中排除指定节点及其所有子孙的查询方法

本教程详细介绍了如何在 Laravel 中处理具有递归关系的数据模型,特别是如何查询并排除某个指定节点及其所有子孙节点。通过自定义 Eloquent 作用域和辅助方法,我们将实现一个高效且可复用的解决方案,帮助开发者在复杂的层级数据结构中精准筛选数据。

1. 理解递归数据结构与模型定义

在许多应用场景中,数据之间存在层级或树状关系,例如分类、评论回复、组织架构等。本教程以“爱好”(Hobbies)为例,其表结构包含 id、name 和 parent_id,其中 parent_id 指向自身表的 id,形成了典型的递归关系。

数据库表结构 (hobbies):

CREATE TABLE hobbies (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    parent_id INT NULL,
    -- 其他字段...
    FOREIGN KEY (parent_id) REFERENCES hobbies(id) ON DELETE CASCADE
);

Eloquent 模型定义 (App\Models\Hobbies.php):

为了方便地在模型中操作这些递归关系,我们需要定义相应的 Eloquent 关联:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder; // 引入Builder

class Hobbies extends Model
{
    // 子爱好(直接子级)
    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');
    }

    // ... 其他方法和属性
}

allsub() 关系通过 with('allsub') 实现了递归加载,使得我们可以一次性获取某个爱好及其所有层级的子孙。

2. 核心需求:排除指定节点及其所有子孙

我们的目标是,给定一个爱好 id,查询出所有不属于该爱好及其任何子孙的爱好记录。例如,如果爱好结构如下:

- 爱好 1
  - 爱好 11
  - 爱好 12
    - 爱好 121
    - 爱好 122
  - 爱好 13
- 爱好 2
  - 爱好 21
  - 爱好 22
    - 爱好 221
    - 爱好 222
  - 爱好 23

如果我们指定排除“爱好 1”,那么最终结果应该包含“爱好 2”及其所有子孙,“爱好 3”及其所有子孙,但不包含“爱好 1”、“爱好 11”、“爱好 12”、“爱好 121”、“爱好 122”、“爱好 13”。

3. 实现方案:自定义作用域与辅助方法

为了实现上述需求,我们将采用以下策略:

  1. 首先,获取指定父节点及其所有递归子孙节点。
  2. 将这些嵌套的数据结构扁平化,提取出所有需要排除的 id 列表。
  3. 使用 Laravel Eloquent 的 whereNotIn 方法,从总数据集中排除这些 id。

我们将通过在 Hobbies 模型中添加一个查询作用域(Scope)和一个私有辅助方法来封装此逻辑。

3.1 辅助方法:扁平化嵌套结果 (flatten)

由于 with('allsub') 返回的是一个嵌套的对象结构,我们需要一个方法来遍历这个结构,并提取出所有节点的 id。

<?php

namespace App\Models;

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

class Hobbies extends Model
{
    // ... 之前定义的关联关系 ...

    /**
     * 扁平化嵌套的数组结构,提取所有非数组/非对象元素(通常是模型属性)。
     * 适用于处理 Eloquent 模型的 toArray() 结果。
     *
     * @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));
            } elseif (is_object($item) && method_exists($item, 'toArray')) {
                // 如果是 Eloquent 模型对象,先转换为数组再处理
                $itemArray = $item->toArray();
                $result[] = array_filter($itemArray, function($value) {
                    return !is_array($value) && !is_object($value);
                });
                $result = array_merge($result, $this->flatten($itemArray));
            }
        }
        // 过滤掉空数组,确保返回纯粹的数据行
        return array_filter($result);
    }
}

3.2 查询作用域:排除指定线路 (scopeIsNotLine)

现在,我们来创建核心的查询作用域 scopeIsNotLine。

<?php

namespace App\Models;

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

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

    /**
     * 查询所有不属于给定ID及其子孙节点的爱好。
     *
     * @param Builder $query Eloquent 查询构建器实例
     * @param int $id 要排除的父节点的ID
     * @return Builder
     */
    public function scopeIsNotLine(Builder $query, int $id): Builder
    {
        // 1. 获取指定ID的爱好及其所有子孙(通过allsub递归加载)
        // toArray() 将模型集合转换为嵌套数组,方便 flatten 方法处理
        $hobbiesToExclude = Hobbies::with('allsub')->where('id', $id)->get()->toArray();

        // 2. 扁平化结果,获取所有需要排除的爱好记录(去除嵌套关系数据)
        $flattenedHobbies = $this->flatten($hobbiesToExclude);

        // 3. 从扁平化结果中提取所有需要排除的爱好ID
        $excludeIds = collect($flattenedHobbies)->map(function ($item) {
            return $item['id'] ?? null; // 确保id存在
        })->filter() // 过滤掉 null 值
          ->unique() // 确保ID唯一
          ->values() // 重置数组索引
          ->all();

        // 4. 将原始父节点ID也加入到排除列表中,以防其未在flattenedHobbies中被直接提取
        $excludeIds[] = $id;
        $excludeIds = array_unique($excludeIds); // 再次去重,确保最终列表的唯一性

        // 5. 使用 whereNotIn 排除这些ID
        return $query->whereNotIn('id', $excludeIds);

        // 原始答案中包含 `->whereDoesntHave('is_archive')`,这通常是一个额外的业务逻辑过滤,
        // 与递归排除本身无关。如果你的业务场景需要此条件,请自行添加;否则,可以省略。
    }
}

3.3 使用示例

现在,你可以在控制器或任何需要的地方轻松地使用这个作用域来查询数据:

use App\Models\Hobbies;

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

// $filteredHobbies 将包含所有不属于 ID 为 1 的爱好及其子孙的爱好记录
echo "排除 ID 为 {$excludedParentId} 及其子孙后的爱好列表:\n";
foreach ($filteredHobbies as $hobby) {
    echo "- " . $hobby->name . " (ID: " . $hobby->id . ")\n";
}

4. 注意事项与优化

  • 性能考量:
    • Hobbies::with('allsub')->where('id', $id)->get() 这一步会执行多次查询来递归加载所有子孙(尽管 with 是预加载,但对于深度递归,Eloquent 可能会生成多个查询或在内存中处理大量数据)。对于层级非常深或数据量巨大的递归关系,这可能会导致性能问题。
    • 替代方案: 如果数据库支持 Common Table Expressions (CTE),使用 CTE 进行递归查询通常是更高效和数据库友好的方式来获取所有子孙ID。例如,在 MySQL 8+ 或 PostgreSQL 中,你可以编写一个递归 CTE 来一次性获取所有子孙ID,然后直接在主查询中使用 WHERE id NOT IN (...)。
  • flatten 方法的健壮性:
    • 上面提供的 flatten 方法尝试更通用地处理 Eloquent 模型的 toArray() 输出。在实际应用中,请确保它能正确处理你的数据结构,只提取你需要的标量属性

今天关于《Laravel递归查询排除节点技巧》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于的内容请关注golang学习网公众号!

12306积分客服联系方式查询12306积分客服联系方式查询
上一篇
12306积分客服联系方式查询
百度AI官网入口及使用教程
下一篇
百度AI官网入口及使用教程
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3652次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3914次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3858次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    5025次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    4230次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码