0

0

Laravel递归关系中排除子孙节点的策略

霞舞

霞舞

发布时间:2025-12-01 12:07:42

|

382人浏览过

|

来源于php中文网

原创

laravel递归关系中排除子孙节点的策略

本教程详细阐述了在Laravel中处理具有递归关系的模型时,如何有效地排除特定节点及其所有子孙节点。通过自定义模型作用域和辅助函数,我们展示了一种从自引用表中查询数据并过滤掉指定层级分支的方法,涉及递归加载、数据扁平化及`whereNotIn`条件的应用,确保查询结果不包含目标节点及其所有后代。

理解递归关系模型

在许多应用场景中,数据实体可能存在自引用关系,形成树状或图状结构。例如,一个“爱好”分类可能包含子爱好,子爱好又可以有自己的子爱好。为了在Laravel中表示这种关系,我们通常会在数据表中包含一个parent_id字段,并在模型中定义递归关系。

假设我们有一个hobbies表,结构如下:

- id
- name
- parent_id

对应的Hobbies模型需要定义以下关系来支持递归操作:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

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');
    }
}

这些关系定义允许我们方便地查询一个爱好的直接子爱好、父爱好,以及通过with('allsub')和with('allparent')递归加载其所有后代或祖先。

排除指定分支的挑战

我们的目标是查询所有爱好,但排除某个特定爱好及其所有子孙爱好。例如,如果我们有一个爱好树:

- 爱好 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”。直接使用whereNotIn需要一个包含所有这些ID的列表,而这个列表是动态且递归生成的。

解决方案:自定义作用域与数据扁平化

为了解决这个问题,我们可以结合使用模型作用域(Scope)和自定义的递归扁平化函数。

核心思路:

阿里云AI平台
阿里云AI平台

阿里云AI平台

下载
  1. 首先,获取目标爱好及其所有子孙爱好。
  2. 将这些嵌套结构的数据扁平化,提取出所有相关爱好的ID。
  3. 使用whereNotIn条件将这些ID从最终查询结果中排除。

1. 扁平化嵌套结果的辅助函数

由于Eloquent的with()方法会返回嵌套的对象结构,我们需要一个函数来遍历这些嵌套数据并提取所有非数组字段(特别是id),从而生成一个扁平的数组。

将以下flatten方法添加到Hobbies模型中:

// Hobbies.php
// ...
class Hobbies extends Model
{
    // ... (现有关系定义)

    /**
     * 递归地将嵌套数组结果扁平化,只保留非数组字段。
     * 适用于将Eloquent的with()结果转换为可操作的扁平数组。
     *
     * @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);
                });
                // 递归处理子项
                $result = array_merge($result, $this->flatten($item));
            }
        }
        // 过滤掉空数组,确保结果只包含有数据的项
        return array_filter($result);
    }
}

这个flatten函数会遍历传入的数组,如果遇到子数组,它会递归地处理,并将其非数组元素提取出来。

2. 实现排除逻辑的模型作用域

接下来,在Hobbies模型中定义一个局部作用域scopeIsNotLine。这个作用域将封装排除特定分支的所有逻辑。

// Hobbies.php
// ...
class Hobbies extends Model
{
    // ... (现有关系定义和flatten方法)

    /**
     * 作用域:查询所有爱好,但排除指定ID及其所有子孙爱好。
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param int $id 要排除的爱好及其子孙的根ID
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeIsNotLine($query, int $id)
    {
        // 1. 获取要排除的爱好及其所有子孙爱好
        // toArray() 将模型集合转换为数组,便于后续的扁平化处理
        $hobbiesToExclude = Hobbies::with('allsub')->where('id', $id)->get()->toArray();

        // 2. 将嵌套结果扁平化,提取所有相关爱好的ID
        $flattenedItems = collect($this->flatten($hobbiesToExclude));

        // 3. 从扁平化结果中提取所有ID
        $excludeIds = $flattenedItems->map(function ($item) {
            return collect($item)->only(['id'])->all();
        })->flatten()->all();

        // 4. 使用 whereNotIn 排除这些ID
        // 额外条件:whereDoesntHave('is_archive') 示例,可根据实际需求调整或移除
        return $query->whereNotIn('id', $excludeIds);
    }
}

作用域scopeIsNotLine的详细步骤:

  1. Hobbies::with('allsub')-youjiankuohaophpcnwhere('id', $id)->get()->toArray();
    • 首先,通过with('allsub')递归加载指定$id的爱好及其所有子孙爱好。
    • get()执行查询并返回一个Eloquent集合。
    • toArray()将集合及其所有嵌套关系转换为纯PHP数组,这是我们自定义flatten函数能够处理的格式。
  2. collect($this->flatten($hobbiesToExclude))
    • 调用模型内的flatten方法,将上一步得到的嵌套数组扁平化。
    • collect()将扁平化后的数组再次转换为Laravel Collection,方便链式操作。
  3. $flattenedItems->map(...)
    • 遍历扁平化后的Collection,对于每个爱好项,只提取其id属性。
    • flatten()->all()将所有提取出的ID列表再次扁平化为一个简单的数字数组。
  4. $query->whereNotIn('id', $excludeIds)
    • 最后,将包含所有要排除ID的数组传递给whereNotIn方法,从而过滤掉这些爱好。
    • whereDoesntHave('is_archive')是原始答案中包含的一个额外条件,用于排除那些有is_archive关系(即已归档)的爱好。如果你的应用没有这个需求,可以将其移除。

如何使用

现在,你可以在任何地方通过链式调用这个作用域来查询数据:

use App\Models\Hobbies;

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

// $filteredHobbies 将包含所有爱好,但不包括 ID 为 1 的爱好及其所有后代。

注意事项与优化

  1. 性能考虑: 对于非常深或非常宽的递归树,with('allsub')可能会导致大量的数据库查询(N+1问题)或返回非常大的数据集。虽然Eloquent会优化with的查询,但toArray()和PHP层面的flatten操作仍可能消耗较多内存和CPU。
    • 数据库递归CTE (Common Table Expressions): 对于性能要求极高或数据集非常庞大的场景,使用数据库原生的递归CTE(如MySQL 8+、PostgreSQL、SQL Server等支持)可能是更优的选择。这能将整个递归查询和排除逻辑下推到数据库层面执行,效率更高。
  2. flatten函数的通用性: 提供的flatten函数是为当前特定场景设计的,它会过滤掉所有数组类型的子项,只保留非数组属性。如果你的嵌套结构更复杂,可能需要调整flatten的逻辑。
  3. 错误处理: 确保传入isNotLine作用域的$id是有效的。如果ID不存在,查询将不会返回任何要排除的ID,这可能导致意外的结果(即没有排除任何项)。
  4. 缓存: 如果递归树结构不经常变化,可以考虑对排除ID列表进行缓存,以减少重复计算。
  5. whereDoesntHave('is_archive'): 这是原始问题中一个额外的过滤条件,与递归排除本身关系不大。在实际应用中,应根据业务需求决定是否保留或替换为其他条件。

总结

通过在Laravel模型中定义递归关系、一个自定义的扁平化辅助函数以及一个模型作用域,我们成功实现了一个灵活且可重用的解决方案,用于在查询中排除特定节点及其所有子孙节点。这种方法在许多需要处理层级数据的应用中都非常实用,为复杂的递归数据过滤提供了一种清晰且易于理解的实现方式。在实际部署时,应根据具体的数据规模和性能要求,权衡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中间件的相关内容,可以阅读本专题下面的文章。

293

2024.04.09

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

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

773

2024.04.09

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

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

385

2024.04.10

laravel入门教程
laravel入门教程

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

141

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 后端服务体系。

570

2026.03.04

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

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

26

2026.03.13

热门下载

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

精品课程

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

共48课时 | 2.5万人学习

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号