0

0

Laravel 递归模型:实现排除特定祖先及其所有后代记录的查询

心靈之曲

心靈之曲

发布时间:2025-12-02 11:11:00

|

236人浏览过

|

来源于php中文网

原创

Laravel 递归模型:实现排除特定祖先及其所有后代记录的查询

本教程详细介绍了如何在 laravel 递归关系中,高效地查询并排除指定节点及其所有子孙节点的数据。通过定义 eloquent 模型中的递归关系,并结合自定义的 scope 方法和辅助函数,我们能够从复杂的层次结构数据中,精确地过滤掉特定分支,实现灵活的数据检索。文章涵盖了模型设置、核心逻辑实现、代码示例及性能优化考量。

Laravel 递归关系模型设置

在处理具有父子关系的层级数据时,Laravel Eloquent 提供了强大的递归关系定义能力。假设我们有一个 hobbies 表,其结构如下:

- id
- name
- parent_id

其中 parent_id 字段指向其父级爱好。为了在 Eloquent 模型中表示这种递归关系,我们需要在 Hobbies 模型中定义相应的关联方法:

// app/Models/Hobbies.php
hasMany(Hobbies::class, 'parent_id');
    }

    /**
     * 获取当前爱好的父爱好。
     */
    public function parent_hobbies()
    {
        return $this->belongsTo(Hobbies::class, 'parent_id');
    }

    /**
     * 递归获取当前爱好的所有子孙爱好。
     * 使用 with('allsub') 实现无限层级预加载。
     */
    public function allsub()
    {
        return $this->sub_hobbies()->with('allsub');
    }

    /**
     * 递归获取当前爱好的所有祖先爱好。
     * 使用 with('allparent') 实现无限层级预加载。
     */
    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”。

来福FM
来福FM

来福 - 你的私人AI电台

下载

解决方案实现

为了实现上述目标,我们可以在 Hobbies 模型中添加一个局部作用域(Scope)方法 scopeIsNotLine 和一个私有辅助函数 flatten。

核心思路

  1. 获取排除列表: 首先,根据给定的ID,使用 allsub 关系递归地获取该爱好及其所有子孙爱好。
  2. 扁平化数据: 将获取到的嵌套结果转换成一个包含所有相关爱好ID的扁平数组。
  3. 执行查询: 使用 whereNotIn 条件,从所有爱好中排除这些ID。

代码实现

app/Models/Hobbies.php 模型中添加以下方法:

// app/Models/Hobbies.php

class Hobbies extends Model
{
    // ... 其他已定义的方法

    /**
     * 局部作用域:查询不属于指定爱好及其子孙链的所有爱好。
     *
     * @param \Illuminate\Database\Eloquent\Builder $query
     * @param int $id 要排除的根爱好ID
     * @return \Illuminate\Database\Eloquent\Builder
     */
    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');
    }

    /**
     * 辅助函数:将嵌套的递归结果扁平化为包含所有节点(非嵌套)的数组。
     *
     * 该函数会遍历输入的数组,提取每个数组元素(代表一个爱好节点)的非数组属性,
     * 并递归处理其内部的嵌套数组(如 'sub_hobbies')。
     *
     * @param array $array 嵌套的爱好数据数组
     * @return array 扁平化的爱好节点数组
     */
    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 及其所有子孙爱好之外的所有爱好。

注意事项与优化

  1. flattenRecursiveData 辅助函数: 这个函数负责将 Laravel with 预加载出来的嵌套数组结构扁平化。它的工作原理是遍历每一个层级的节点,提取其自身的标量属性,并递归地处理其包含的子数组(例如 sub_hobbies 关系)。最终,collect(...)->map(...)->flatten()->unique()->all() 链式操作将这些扁平化的节点转换为唯一的ID列表。
  2. 性能考量:
    • N+1 问题: Hobbies::with('allsub') 语句本身会通过预加载解决 N+1 问题,但对于非常深的递归层级和大量数据,一次性加载整个分支到内存中可能会消耗较多资源。
    • 数据库效率: 对于支持 CTE(Common Table Expressions,如 MySQL 8+, PostgreSQL, SQL Server)的数据库,使用 CTE 可以更高效地在数据库层面进行递归查询和过滤,减少应用层的数据处理负担。例如,可以使用 CTE 递归地找出所有要排除的ID,然后直接在主查询中使用 NOT IN。
  3. 通用性: scopeIsNotLine 中的 whereDoesntHave('is_archive') 是一个额外的条件,用于排除那些没有 is_archive 关系的爱好。如果你的应用没有这个需求,可以将其移除。
  4. 替代方案:
    • CTE (Common Table Expressions): 对于大型或深度递归的数据集,考虑使用数据库的 CTE 功能。你可以在 Laravel 中通过 DB::raw 或编写更复杂的 Eloquent 查询来实现。
    • 预排序遍历树 (Nested Set Model) 或路径枚举 (Path Enumeration): 如果层级结构非常深且查询频繁,可以考虑在数据库层面采用这些专门的树结构存储方案,它们能极大地优化树形结构查询的性能。

总结

通过在 Laravel Eloquent 模型中定义递归关系,并结合自定义的局部作用域和辅助函数,我们可以有效地处理复杂的层级数据查询需求,例如排除特定分支及其所有子孙节点。这种方法保持了代码的清晰性和 Eloquent 的优雅

相关专题

更多
php文件怎么打开
php文件怎么打开

打开php文件步骤:1、选择文本编辑器;2、在选择的文本编辑器中,创建一个新的文件,并将其保存为.php文件;3、在创建的PHP文件中,编写PHP代码;4、要在本地计算机上运行PHP文件,需要设置一个服务器环境;5、安装服务器环境后,需要将PHP文件放入服务器目录中;6、一旦将PHP文件放入服务器目录中,就可以通过浏览器来运行它。

2631

2023.09.01

php怎么取出数组的前几个元素
php怎么取出数组的前几个元素

取出php数组的前几个元素的方法有使用array_slice()函数、使用array_splice()函数、使用循环遍历、使用array_slice()函数和array_values()函数等。本专题为大家提供php数组相关的文章、下载、课程内容,供大家免费下载体验。

1630

2023.10.11

php反序列化失败怎么办
php反序列化失败怎么办

php反序列化失败的解决办法检查序列化数据。检查类定义、检查错误日志、更新PHP版本和应用安全措施等。本专题为大家提供php反序列化相关的文章、下载、课程内容,供大家免费下载体验。

1511

2023.10.11

php怎么连接mssql数据库
php怎么连接mssql数据库

连接方法:1、通过mssql_系列函数;2、通过sqlsrv_系列函数;3、通过odbc方式连接;4、通过PDO方式;5、通过COM方式连接。想了解php怎么连接mssql数据库的详细内容,可以访问下面的文章。

952

2023.10.23

php连接mssql数据库的方法
php连接mssql数据库的方法

php连接mssql数据库的方法有使用PHP的MSSQL扩展、使用PDO等。想了解更多php连接mssql数据库相关内容,可以阅读本专题下面的文章。

1418

2023.10.23

html怎么上传
html怎么上传

html通过使用HTML表单、JavaScript和PHP上传。更多关于html的问题详细请看本专题下面的文章。php中文网欢迎大家前来学习。

1234

2023.11.03

PHP出现乱码怎么解决
PHP出现乱码怎么解决

PHP出现乱码可以通过修改PHP文件头部的字符编码设置、检查PHP文件的编码格式、检查数据库连接设置和检查HTML页面的字符编码设置来解决。更多关于php乱码的问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1447

2023.11.09

php文件怎么在手机上打开
php文件怎么在手机上打开

php文件在手机上打开需要在手机上搭建一个能够运行php的服务器环境,并将php文件上传到服务器上。再在手机上的浏览器中输入服务器的IP地址或域名,加上php文件的路径,即可打开php文件并查看其内容。更多关于php相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1306

2023.11.13

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

43

2026.01.16

热门下载

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

精品课程

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

共48课时 | 1.8万人学习

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

共3课时 | 0.3万人学习

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

共1课时 | 797人学习

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

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