You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于Eloquent模型,利用闭包表遍历树形结构并快速生成嵌套JSON

如何基于Eloquent模型,利用闭包表遍历树形结构并快速生成嵌套JSON

首先得给你提个小修正:你的FilesClosure模型里的关联写错了,ancestor和decedant应该是BelongsTo关联(因为闭包表的字段是外键指向files.id),而不是HasOne,先把这个改过来,避免后续关联出错:

use Illuminate\Database\Eloquent\Relations\BelongsTo;

class FilesClosure extends Model
{
    protected $table='files_closure';
    public $timestamps = false;

    public function ancestor(): BelongsTo
    {
        return $this->belongsTo(Files::class, 'ancestor', 'id');
    }

    public function decedant(): BelongsTo
    {
        return $this->belongsTo(Files::class, 'decedant', 'id');
    }
}

另外,Files模型里的parentFolder关联也应该是BelongsTo,因为folder字段是当前文件的父ID,指向父文件的id:

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Files extends Model
{
    protected $table='files';
    public $timestamps = false;

    public function parentFolder(): BelongsTo
    {
        return $this->belongsTo(self::class, 'folder', 'id');
    }

    public function files(): HasMany
    {
        return $this->hasMany(self::class, 'folder');
    }
}

好了,回到你的核心需求:用闭包表快速生成嵌套JSON,避免递归查询的性能问题。闭包表的优势就是能一次性拉取所有层级的关系,我们只需要把数据批量查出来,在内存里构建嵌套结构就行,这样只会产生2-3次批量查询,完全没有N+1的问题,大数据集下性能会好很多。

具体实现步骤

1. 批量拉取所有需要的数据

我们先获取所有顶层节点(folder为null的文件/文件夹),然后通过闭包表获取它们所有的后代关系,再一次性拉取所有涉及到的文件数据:

// 1. 获取所有顶层根节点的ID
$rootIds = Files::whereNull('folder')->pluck('id');

// 2. 获取这些根节点对应的所有闭包关系(祖先-后代),按depth排序(确保先处理浅层级)
$closureRelations = FilesClosure::whereIn('ancestor', $rootIds)
    ->orderBy('depth')
    ->get();

// 3. 收集所有需要的文件ID:根节点ID + 所有后代ID
$allFileIds = $rootIds->merge(
    $closureRelations->pluck('decedant')
)->unique();

// 4. 一次性拉取所有文件,用ID作为键,方便后续快速查找
$allFiles = Files::whereIn('id', $allFileIds)->keyBy('id');

2. 在内存中构建嵌套结构

接下来我们遍历闭包关系,把每个后代节点挂载到对应的父节点下。因为我们按depth排序了,所以父节点一定在子节点之前被处理,不会出现找不到父节点的情况:

// 初始化顶层节点的集合,给每个节点添加空的files集合用来存子节点
$nestedFiles = $allFiles->filter(function ($file) use ($rootIds) {
    return $rootIds->contains($file->id);
})->map(function ($file) {
    // 只有文件夹需要files集合,文件可以跳过
    if ($file->type === 'FOLDER') {
        $file->files = collect();
    }
    return $file;
})->keyBy('id');

// 遍历闭包关系,把子节点挂载到父节点的files集合中
foreach ($closureRelations as $relation) {
    // 找到父节点
    $parent = $nestedFiles[$relation->ancestor];
    // 找到当前子节点
    $child = $allFiles[$relation->decedant];
    
    // 给文件夹类型的子节点初始化files集合
    if ($child->type === 'FOLDER' && !isset($child->files)) {
        $child->files = collect();
    }
    
    // 把子节点添加到父节点的files里
    $parent->files->push($child);
    // 把子节点也加入到nestedFiles集合,方便后续更深层级的挂载
    $nestedFiles[$child->id] = $child;
}

// 最后把顶层节点转成JSON即可
$json = $nestedFiles->values()->toJson();

为什么这个方案高效?

  • 查询次数极少:总共只有3次批量查询(获取根节点ID、闭包关系、所有文件),完全避免了递归with('files')带来的N+1查询问题。
  • 内存处理快:所有嵌套逻辑都在内存中完成,集合操作在PHP里是非常高效的,比多次数据库请求快得多。
  • 适配大数据集:不管你的文件树有多深,查询次数都是固定的,不会随着层级增加而变慢。

注意事项

  • 确保你的闭包表数据是正确的:新增、删除、移动文件/文件夹时,一定要同步更新files_closure表的ancestor、decedant和depth字段,否则构建的嵌套结构会出错。
  • 如果需要更精简的JSON,可以在转JSON前移除空的files字段(比如文件类型的节点),可以用集合的transform方法处理。

备注:内容来源于stack exchange,提问作者Dimitrios Desyllas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.20 08:43:05