0

0

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

心靈之曲

心靈之曲

发布时间:2025-12-02 12:54:18

|

456人浏览过

|

来源于php中文网

原创

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

听脑AI
听脑AI

听脑AI语音,一款专注于音视频内容的工作学习助手,为用户提供便捷的音视频内容记录、整理与分析功能。

下载
// 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 端的处理方式对于中小型数据集非常有效,但在处理大规模或深度层级数据时,建议考虑数据库层面的优化方案以获得更好的性能。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
laravel组件介绍
laravel组件介绍

laravel 提供了丰富的组件,包括身份验证、模板引擎、缓存、命令行工具、数据库交互、对象关系映射器、事件处理、文件操作、电子邮件发送、队列管理和数据验证。想了解更多laravel的相关内容,可以阅读本专题下面的文章。

340

2024.04.09

laravel中间件介绍
laravel中间件介绍

laravel 中间件分为五种类型:全局、路由、组、终止和自定。想了解更多laravel中间件的相关内容,可以阅读本专题下面的文章。

294

2024.04.09

laravel使用的设计模式有哪些
laravel使用的设计模式有哪些

laravel使用的设计模式有:1、单例模式;2、工厂方法模式;3、建造者模式;4、适配器模式;5、装饰器模式;6、策略模式;7、观察者模式。想了解更多laravel的相关内容,可以阅读本专题下面的文章。

774

2024.04.09

thinkphp和laravel哪个简单
thinkphp和laravel哪个简单

对于初学者来说,laravel 的入门门槛较低,更易上手,原因包括:1. 更简单的安装和配置;2. 丰富的文档和社区支持;3. 简洁易懂的语法和 api;4. 平缓的学习曲线。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

386

2024.04.10

laravel入门教程
laravel入门教程

本专题整合了laravel入门教程,想了解更多详细内容,请阅读专题下面的文章。

144

2025.08.05

laravel实战教程
laravel实战教程

本专题整合了laravel实战教程,阅读专题下面的文章了解更多详细内容。

85

2025.08.05

laravel面试题
laravel面试题

本专题整合了laravel面试题相关内容,阅读专题下面的文章了解更多详细内容。

80

2025.08.05

PHP高性能API设计与Laravel服务架构实践
PHP高性能API设计与Laravel服务架构实践

本专题围绕 PHP 在现代 Web 后端开发中的高性能实践展开,重点讲解基于 Laravel 框架构建可扩展 API 服务的核心方法。内容涵盖路由与中间件机制、服务容器与依赖注入、接口版本管理、缓存策略设计以及队列异步处理方案。同时结合高并发场景,深入分析性能瓶颈定位与优化思路,帮助开发者构建稳定、高效、易维护的 PHP 后端服务体系。

612

2026.03.04

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

26

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
MySQL 教程
MySQL 教程

共48课时 | 2.6万人学习

MySQL 初学入门(mosh老师)
MySQL 初学入门(mosh老师)

共3课时 | 0.3万人学习

简单聊聊mysql8与网络通信
简单聊聊mysql8与网络通信

共1课时 | 850人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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