
本文详细介绍了在 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');
}
// ... 其他方法和作用域将在此处添加
}在上述模型中:
我们的目标是:给定一个特定爱好的 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)和辅助方法来完成。
// 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 方法是解决问题的关键之一。由于 with('allsub') 加载的数据是一个嵌套的数组结构(因为关系是递归的),我们需要一个方法来遍历这个嵌套结构,并提取出所有独立的爱好项,无论它们位于哪一层。
这个作用域是主要的查询逻辑:
获取要排除的爱好及其子孙线: Hobbies::with('allsub')-youjiankuohaophpcnwhere('id', $id)->get()->toArray(); 这一步首先通过 with('allsub') 加载指定 ID 的爱好及其所有递归子孙。.get()->toArray() 将 Eloquent 集合转换为 PHP 数组,以便后续的扁平化处理。
扁平化结果并提取 ID: $flattenedHobbies = collect($this->flatten($hobbiesToExclude));$excludeIds = $flattenedHobbies->map(...)->filter()->unique()->all(); 这里利用 flatten 方法将复杂的嵌套数组结构转换为一个简单的数组,其中每个元素都是一个独立的爱好数据数组。接着,通过 Laravel 的集合方法 map 提取出每个爱好的 id,filter 移除可能存在的空值,unique 确保 ID 不重复,最后 all() 转换为纯 PHP 数组。
使用 whereNotIn 排除: $query->whereNotIn('id', $excludeIds); 这是 Eloquent 的核心功能,它会修改当前查询,使其只返回 ID 不在 $excludeIds 列表中的爱好。
额外过滤条件(可选): $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";
}性能考虑:
flatten 方法的健壮性: 当前 flatten 方法假设模型属性是标量或简单的数组。如果模型关系中包含更复杂的对象或集合,可能需要调整 array_filter 的条件。
替代方案:闭包表或路径枚举: 对于非常复杂的递归关系查询,更专业的解决方案是使用 闭包表 (Closure Table) 或 路径枚举 (Path Enumeration) 模式。这些模式通过额外的辅助表或字段来存储节点间的路径信息,从而将递归查询转换为简单的非递归查询,极大地提高查询效率和灵活性。Laravel 社区也有一些包实现了这些模式。
本教程提供了一种在 Laravel 中处理递归关系并排除特定分支的实用方法。通过结合 Eloquent 的递归关系加载、自定义的扁平化函数和查询作用域,我们能够有效地实现复杂的层级数据过滤需求。尽管这种 PHP 端的处理方式对于中小型数据集非常有效,但在处理大规模或深度层级数据时,建议考虑数据库层面的优化方案以获得更好的性能。
以上就是Laravel 递归查询:高效排除指定父级及其所有子孙节点的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号