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

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

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

Laravel 递归关系模型设置

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

- id- name- parent_id

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

// app/Models/Hobbies.phphasMany(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”。

解决方案实现

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

核心思路

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

代码实现

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

// app/Models/Hobbies.phpclass Hobbies extends Model{    // ... 其他已定义的方法    /**     * 局部作用域:查询不属于指定爱好及其子孙链的所有爱好。     *     * @param IlluminateDatabaseEloquentBuilder $query     * @param int $id 要排除的根爱好ID     * @return IlluminateDatabaseEloquentBuilder     */    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 AppModelsHobbies;// 假设要排除的爱好ID是 1$hobbies = Hobbies::isNotLine(1)->get();// $hobbies 集合中将包含除了 ID 为 1 及其所有子孙爱好之外的所有爱好。

注意事项与优化

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

总结

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

以上就是Laravel 递归模型:实现排除特定祖先及其所有后代记录的查询的详细内容,更多请关注php中文网其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1339798.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
如何在 Laravel Eloquent 中获取带条件的关联模型计数
上一篇 2025年12月13日 03:16:15
PHP中高效提取指定HTML标题及其紧邻段落的教程
下一篇 2025年12月13日 03:16:26

相关推荐

  • composer require-dev和require有什么不同_Composer Require与Require-Dev区别解析

    require用于声明项目运行必需的依赖,如框架、数据库组件和第三方SDK,这些包会随项目部署到生产环境;2. require-dev用于声明仅在开发和测试阶段需要的工具,如PHPUnit、PHPStan、Faker等,不会默认部署到生产环境;3. 安装时composer install根据环境决定…

    2026年5月10日
    1000
  • 开源免费PHP工具 PHP开发效率提升利器

    推荐开源免费PHP开发工具以提升效率:VS Code、Sublime Text轻量高效,PhpStorm专业强大;调试用Xdebug、Kint、Ray;依赖管理选Composer;代码质量工具包括PHPStan、Psalm、PHP_CodeSniffer;数据库管理可用%ignore_a_1%MyA…

    2026年5月10日
    000
  • Matplotlib 地图中多类型图例的创建与优化

    Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化

    本教程旨在解决matplotlib地图可视化中,如何在一个图例中同时展示颜色块(如区域分类)和自定义标记(如特定兴趣点)的问题。文章详细介绍了当传统`patch`对象无法正确显示标记时,如何利用`matplotlib.lines.line2d`创建标记图例句柄,并将其与颜色块图例句柄合并,从而生成一…

    2026年5月10日 用户投稿
    900
  • 怎么在PHP代码中实现图片上传功能_PHP图片上传功能实现与安全处理教程

    首先创建含enctype的HTML表单,再用PHP接收文件,检查目录、移动临时文件,验证类型与大小,生成唯一文件名,并调整php.ini限制以确保上传成功。 如果您尝试在PHP项目中添加图片上传功能,但服务器无法正确接收或保存文件,则可能是由于表单配置、文件处理逻辑或安全限制的问题。以下是实现该功能…

    2026年5月10日
    300
  • 获取日期中的周数:CodeIgniter 教程

    本教程旨在帮助开发者在 CodeIgniter 框架中,从日期字符串中准确提取周数。我们将使用 PHP 内置的 DateTime 类,并提供详细的代码示例和注意事项,确保您能够轻松地在项目中实现此功能。 使用 DateTime 类获取周数 PHP 的 DateTime 类提供了一种便捷的方式来处理日…

    2026年5月10日
    100
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • RichHandler与Rich Progress集成:解决显示冲突的教程

    在使用rich库的`richhandler`进行日志输出并同时使用`progress`组件时,可能会遇到显示错乱或溢出问题。这通常是由于为`richhandler`和`progress`分别创建了独立的`console`实例导致的。解决方案是确保日志处理器和进度条组件共享同一个`console`实例…

    2026年5月10日
    300
  • php常量怎么用_PHP常量(define/const)定义与使用方法

    PHP中可通过define函数和const关键字定义常量,用于存储不可变值。define适用于全局作用域,支持动态名称和条件定义,如define(‘SITE_NAME’, ‘MyWebsite’);const在编译时生效,语法简洁但限制多,只能在类或全…

    2026年5月10日
    000
  • 深入理解 Express.js 中 next() 参数的作用与中间件机制

    本文深入探讨 express.js 中间件函数中的 `next()` 参数。它负责将控制权传递给请求-响应周期中的下一个中间件或路由处理程序。文章将详细解释 `next()` 的工作原理、中间件的注册与执行顺序,以及不正确使用 `next()` 可能导致请求挂起的风险,并通过代码示例和实际应用场景,…

    2026年5月10日
    000
  • 使用 WebCodecs VideoDecoder 实现精确逐帧回退

    本文档旨在解决在使用 WebCodecs VideoDecoder 进行视频解码时,实现精确逐帧回退的问题。通过比较帧的时间戳与目标帧的时间戳,可以避免渲染中间帧,从而提高用户体验。本文将提供详细的解决方案和示例代码,帮助开发者实现精确的视频帧控制。 在使用 WebCodecs VideoDecod…

    2026年5月10日
    300
  • PHP动态生成表单输入与POST数据获取实践指南

    本教程详细阐述了如何在php中根据动态数据源(如数据库值)生成多个表单输入框,并演示了如何通过post方法准确无误地获取这些动态生成的输入值。文章强调了正确的输入框命名策略,避免了常见的命名误区,并提供了完整的代码示例,确保开发者能够高效处理动态表单数据。 动态生成表单输入 在Web开发中,我们经常…

    2026年5月10日
    000
  • JavaScript 闭包:理解闭包原理与内存泄漏问题

    闭包是函数访问其外部作用域变量的能力,即使外部函数已执行完毕。如 inner 函数引用 outer 中的 count,形成闭包,使变量持久存在。闭包本身无害,但可能因延长变量生命周期导致内存泄漏,例如事件监听器引用大对象时。若未及时清理 DOM 事件或定时器,闭包会阻止垃圾回收,造成内存占用过高。解…

    2026年5月10日
    100
  • html5怎么画实线_HTML5用CSS border-style:solid画元素实线边框【绘制】

    可通过CSS的border-style属性设为solid添加实线边框:一、内联样式用border:2px solid #000;二、内部样式表统一设置如div{border:1px solid #333};三、外部CSS文件定义.my-box{border:3px solid red}并引入;四、单…

    2026年5月10日
    400
  • JavaScript函数中插入加载动画(Spinner)的正确方法

    本文旨在解决在JavaScript函数中插入加载动画(Spinner)时遇到的异步问题。通过引入async/await和Promise.all,确保在数据处理完成前后正确显示和隐藏加载动画,提升用户体验。我们将提供两种实现方案,并详细解释其原理和优势。 在Web开发中,当执行耗时操作时,显示加载动画…

    2026年5月10日
    500
  • JS如何实现迭代器?迭代器协议

    JavaScript中实现迭代器需遵循可迭代协议和迭代器协议,通过定义[Symbol.iterator]方法返回具备next()方法的迭代器对象,从而支持for…of和展开运算符;该机制统一了数据结构的遍历接口,实现惰性求值,适用于自定义对象、树、图及无限序列等复杂场景,提升代码通用性与…

    2026年5月10日
    300
  • Golang空接口如何应用在项目中

    空接口可用于接收任意类型值,常见于日志函数、通用数据结构、JSON动态解析及配置驱动逻辑,提升代码灵活性,但需配合类型断言确保安全,避免滥用以降低维护成本。 空接口 interface{} 在 Go 语言中是一个非常灵活的类型,它可以存储任何类型的值。虽然它牺牲了一部分类型安全,但在实际项目中合理使…

    2026年5月10日
    300
  • 使用 Pydantic v2 实现条件性必填字段

    本文介绍了如何在 Pydantic v2 模型中实现条件性必填字段。通过自定义验证器,可以根据模型中其他字段的值来动态地控制某些字段是否为必填项,从而满足 API 交互中数据验证的复杂需求。本文提供了一个具体的示例,展示了如何确保模型中至少有一个字段被赋值。 在 Pydantic v2 中,虽然没有…

    2026年5月10日
    000
  • MySQL数据库不支持中文的解决办法

    接上一篇文章,在解决了mysql+flask环境配置问题之后,往数据库存中文字符串会报1366错误,提示不正确的字符。继而发现默认的mysql采用了latin1字符集,这种编码是不支持中文的。 如果想支持中文的话,需要设置一下mysql字符集。 众所周知utf-8是可以的,gbk也没问题,为了可扩展…

    用户投稿 2026年5月10日
    000
  • React组件中动态属性值的管理与同步:利用状态实现受控组件

    本教程旨在解决react组件中动态属性值同步使用的问题。我们将探讨如何利用react的`usestate` hook来管理组件内部状态,从而实现一个属性的值动态地影响另一个属性,并构建出可预测、易于维护的受控组件。文章将通过具体代码示例,详细阐述从初始化状态到处理状态更新的完整过程,并强调受控组件在…

    2026年5月10日
    000
  • 如何讲html和css_讲解HTML与CSS结合使用基础【基础】

    需将HTML与CSS结合使用以实现网页结构与样式的分离:HTML定义标题、段落等语义结构,CSS控制颜色、字体等外观;可通过内联样式、内部样式表或外部CSS文件引入样式,并利用类选择器和ID选择器精准应用。 如果您希望网页不仅展示内容,还能具备基本的样式和结构布局,则需要将HTML与CSS结合使用。…

    2026年5月10日
    100

发表回复

登录后才能评论
关注微信