如何基于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
相关产品推荐
相关产品推荐

