PAYATHON 2026

Laravel 软删除父记录时如何 soft delete 关联记录?

支付阿杰

结论

Laravel 的软删除不会自动作用于关联模型。对 Invoice 执行 delete() 时,Laravel 只会更新当前记录的 deleted_at。要同时软删除付款记录,需要先定义 payments 关联,再在 Invoice 的删除事件中调用关联记录的 delete()。

定义关联并处理级联软删除

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Invoice extends Model
{
    use SoftDeletes;

    public function payments(): HasMany
    {
        return $this->hasMany(Payment::class);
    }

    protected static function booted(): void
    {
        static::deleting(function (Invoice $invoice) {
            if ($invoice->isForceDeleting()) {
                $invoice->payments()
                    ->withTrashed()
                    ->forceDelete();

                return;
            }

            $invoice->payments()->delete();
        });
    }
}

Payment 模型同样需要启用 SoftDeletes:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Payment extends Model
{
    use SoftDeletes;

    public function invoice(): BelongsTo
    {
        return $this->belongsTo(Invoice::class);
    }
}

之后执行:

$invoice = Invoice::findOrFail($id);
$invoice->delete();

Laravel 会同时更新 invoices.deleted_at 和相关记录的 payments.deleted_at,不会真正删除这些数据。

建议使用事务

如果父记录和关联记录必须同时完成删除,可以放在同一个事务中:

use App\Models\Invoice;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

public function cancel(Request $request, $id)
{
    DB::transaction(function () use ($id) {
        $invoice = Invoice::findOrFail($id);
        $invoice->delete();
    });

    return response()->json([
        'success' => 'OK',
    ]);
}

其中任何一步发生异常,父记录和子记录的修改都会回滚。

如果子记录还需要触发模型事件

$invoice->payments()->delete() 是批量更新,通常不会为每条 Payment 记录触发 deleting、deleted 等模型事件。

如果 Payment 本身有删除监听器,或者还要继续处理更深层的关联,应逐条删除:

static::deleting(function (Invoice $invoice) {
    if ($invoice->isForceDeleting()) {
        $invoice->payments()
            ->withTrashed()
            ->eachById(function (Payment $payment) {
                $payment->forceDelete();
            });

        return;
    }

    $invoice->payments()
        ->eachById(function (Payment $payment) {
            $payment->delete();
        });
});

关联记录较多时,使用 eachById() 可以避免一次性加载全部数据,占用的内存更少。

注意事项

数据库外键的 ON DELETE CASCADE 只在真正执行 DELETE 时生效。软删除只是更新 deleted_at,因此数据库外键无法自动软删除关联记录。

删除时还应使用模型实例:

$invoice->delete();

不要改用批量语句:

Invoice::whereKey($id)->delete();

批量删除不会触发 Invoice 的模型删除事件,因此前面定义的级联逻辑也不会执行。

如果之后需要恢复 Invoice,是否同步恢复 Payment 应由具体业务决定。直接恢复所有关联 Payment,可能会把删除 Invoice 之前已经单独删除的付款记录一并恢复。更稳妥的做法是记录删除批次,或者明确每条关联记录的删除来源。

备注:内容仅供参考。