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

Laravel递归树形删除中deleting事件子节点标题打印异常问题

Laravel自引用模型递归删除异常问题

问题背景

我有一个用于分类树形结构的Laravel自引用模型,每个节点可包含子节点,需要删除节点时连带删除所有后代。

模型关联定义

public function children()
{
    return $this->hasMany(self::class, 'parent_id')->orderBy('sort');
}

树形路径访问器与递归方法

定义访问器用于构建完整树形路径标题:

// Full title
public function getFitleAttribute()
{
    return implode(' > ', array_map(function ($category) {
        return $category->title;
    }, $this->getTreePath()));
}

递归路径方法:

public function getTreePath($model = null)
{
    $path = [];

    if ($model === null) {
        $model = $this;
    }

    $path = $this->getTreePathNode($model);

    return array_reverse($path);
}

public function getTreePathNode($model)
{
    $path[] = $model;

    if ($model->parent) {
        $path = array_merge($path, $this->getTreePathNode($model->parent));
    }

    return $path;
}

控制器中删除逻辑(正常工作)

在控制器执行删除时,日志能正确打印子节点标题:

public function destroy(Category $category)
{
    foreach ($category->children as $child) {
        Log::info('deleting child title: ' . $child->fitle); // 打印正确的子节点标题
        Category::deleteWithChildren($child);
    }

    $category->delete();

    return redirect(route('category.index'))
        ->with('status', __('Category deleted'));
}

递归删除方法:

protected static function deleteWithChildren($category)
{
    foreach ($category->children as $child) {
        self::deleteWithChildren($child);
    }
     $category->delete();
}

移至模型deleting事件后出现异常

将递归删除逻辑移至模型deleting事件后,日志打印错误的子节点标题(显示兄弟节点而非实际子节点):

public static function booted(): void
{
    static::deleting(function ($category) {
        Log::info('deleted title: ' . $category->fitle);

        foreach ($category->children as $child) {
            Log::info('deleting child title: ' . $child->fitle);
            static::deleteWithChildren($child);
        }
    });
}

疑问

原本预期deleting事件中的$category->children始终包含当前模型的直接子节点,请问为何表现不同?Laravel中递归删除自引用树形结构的正确方式是什么,才能避免获取错误的子节点数据?

环境:Laravel Framework 10.48.22


问题原因

核心问题出在Eloquent模型的关联缓存和事件执行顺序:

  1. 在deleting事件中调用static::deleteWithChildren($child)时,删除子节点会触发其自身的deleting事件,但父模型的children关联已加载到内存(缓存)中。子节点被删除后,Eloquent不会自动更新父模型的关联缓存,导致后续遍历父模型children时,拿到的还是初始加载的集合(包含已删除节点),最终引发日志打印错误。
  2. getTreePath方法依赖$model->parent关联,父节点处于deleting事件阶段时,模型状态可能干扰关联加载逻辑,导致路径计算错误。

正确的递归删除方式

方案1:禁用关联缓存,强制从数据库拉取子节点

在deleting事件中,通过关联查询直接从数据库获取最新子节点,绕过缓存:

public static function booted(): void
{
    static::deleting(function ($category) {
        // 强制从数据库获取当前节点的直接子节点
        $children = $category->children()->get();
        
        foreach ($children as $child) {
            Log::info('deleting child title: ' . $child->fitle);
            $child->delete(); // 触发子节点的deleting事件,自动递归删除
        }
    });
}

此时可删除原有的deleteWithChildren静态方法,事件会自动递归触发。

方案2:数据库级联删除(推荐)

如果数据库支持外键约束(如MySQL InnoDB),直接在迁移中设置onDelete('cascade'),让数据库自动处理递归删除,这是最高效且避免缓存问题的方式:

// 分类表迁移文件
public function up()
{
    Schema::create('categories', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->integer('parent_id')->nullable();
        $table->integer('sort')->default(0);
        $table->timestamps();
        
        // 添加外键约束,级联删除子节点
        $table->foreign('parent_id')
              ->references('id')
              ->on('categories')
              ->onDelete('cascade');
    });
}

这种方式下,只需调用$category->delete(),数据库会自动删除所有后代节点,无需手动处理递归。

方案3:清理关联缓存后重新加载

若必须使用Eloquent事件处理,可在遍历前清理模型的关联缓存:

public static function booted(): void
{
    static::deleting(function ($category) {
        // 清理children关联的缓存
        $category->unsetRelation('children');
        $children = $category->children;
        
        foreach ($children as $child) {
            Log::info('deleting child title: ' . $child->fitle);
            $child->delete();
        }
    });
}

额外优化建议

  • 可优化getTreePath方法,比如使用Laravel嵌套集扩展(如kalnoy/nestedset)的ancestors关联,或缓存路径结果避免重复递归查询。
  • 若用事件处理删除,建议先一次性获取所有子节点集合,避免遍历过程中缓存导致的集合变化。

内容的提问来源于stack exchange,提问作者bcExpt1123

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.11 10:23:11