【Laravel】ソフトデリート(SoftDeletes)の使い方

laravelアイキャッチ CakePHP

こんにちは、かつコーチです。

前回はトランザクションでデータ整合性を守る方法を解説しました。

今回は、削除処理を「取り消せる形」にしておくための機能、SoftDeletes(ソフトデリート)を扱います。

「うっかり削除してしまった」というミスは、実務で必ずと言っていいほど起こります。

その備えとして、Laravelには削除を「見た目上だけ消す」仕組みが標準で用意されています。

ソフトデリートとは何か

物理削除との違い

通常、Eloquentモデルに対してdelete()を呼ぶと、対象のレコードはデータベースから完全に消えます。

これを物理削除と呼びます。

<?php

$post = Post::find(1);
$post->delete(); // レコードがテーブルから完全に消える

一方、ソフトデリートとは、レコードを実際には削除せず、「削除された日時」を記録するカラムに値を入れることで、「削除された扱い」にする仕組みです。

データ自体はテーブルに残り続けるため、後から復元したり、削除履歴を確認したりすることができます。

なぜソフトデリートが必要なのか

たとえば会員が退会した場合を考えてみましょう。

会員データを物理削除してしまうと、退会後に「過去の注文履歴を見たい」となったときに、注文とひも付くはずの会員情報が存在しないという事態になります。

また、誤操作で削除してしまった場合、物理削除だとバックアップから復元しない限り元に戻せません。

ソフトデリートを使っておけば、「削除フラグを戻すだけ」で復元できるため、こうした事故への備えとしても有効です。

私自身、開発中の管理画面で削除ボタンを連打していたら、テスト用ではなく本番相当のデータを消してしまいそうになったことがあります。

そのとき、あらかじめソフトデリートを設定していたおかげで、慌てずに復元できた経験があります。

SoftDeletesの導入手順

マイグレーションにdeleted_atカラムを追加する

ソフトデリートを使うには、対象テーブルにdeleted_atというカラムが必要です。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->softDeletes(); // deleted_atカラムを追加
        });
    }

    public function down(): void
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
};

$table->softDeletes()と書くだけで、NULL許容のタイムスタンプ型カラムdeleted_atが追加されます。

このカラムがNULLなら「削除されていない」、日時が入っていれば「その日時に削除された」という意味になります。

モデルにSoftDeletesトレイトを使う

マイグレーションを実行したら、モデル側にSoftDeletesトレイトを追加します。

<?php

namespace App\Models;

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

class Post extends Model
{
    use SoftDeletes;
}

このトレイトを追加するだけで、delete()の挙動がソフトデリートに切り替わります。

<?php

$post = Post::find(1);
$post->delete(); // deleted_atに現在日時が入るだけで、レコードは残る

以降、Post::all()Post::find()などの通常の取得処理は、deleted_atがNULLのレコードだけを自動的に対象にしてくれます。

つまり、削除済みのレコードは特に意識しなくても一覧や検索結果から自動的に除外される、というのがSoftDeletesの便利なところです。

つまずきやすいポイント:ユニーク制約とソフトデリートの組み合わせ

❌ Before:ソフトデリート済みデータとユニーク制約が競合する

私が実際にハマったのが、「メールアドレスにユニーク制約を張ったテーブル」にソフトデリートを組み合わせたケースです。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('members', function (Blueprint $table) {
            $table->id();
            $table->string('email')->unique(); // 単純なunique制約
            $table->softDeletes();
            $table->timestamps();
        });
    }
};

このテーブルで、あるユーザーを一度ソフトデリートしてから、同じメールアドレスで再登録しようとするとエラーになります。

理由は単純で、ソフトデリートしたレコードはdeleted_atに日時が入っているだけでテーブルには残り続けており、emailカラムのユニーク制約はデータベースレベルでは無視してくれないからです。

「退会したユーザーが同じメールアドレスで再登録できない」という不具合として発覚し、原因がユニーク制約だと気づくまでしばらく悩みました。

✅ After:deleted_atを含めた複合ユニーク制約にする

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('members', function (Blueprint $table) {
            $table->id();
            $table->string('email');
            $table->softDeletes();
            $table->timestamps();

            // emailとdeleted_atの組み合わせでユニーク制約にする
            $table->unique(['email', 'deleted_at']);
        });
    }
};

email単体ではなくemaildeleted_atの組み合わせでユニーク制約を作ることで、「削除日時が異なれば同じメールアドレスも許容する」という挙動になります。

ただし、この方法にも注意点があります。

複数回退会・再登録を繰り返すと、deleted_atの日時が毎回異なるため制約はすり抜けられますが、「同時刻に2件の削除」のような極端なケースでは競合が起こり得ます。

再登録の仕様が厳密に必要な場合は、ユニーク制約だけに頼らず、アプリケーション側でも重複チェックのロジックを入れておくと安心です。

SoftDeletesの応用

削除済みデータの取得・復元

ソフトデリートされたデータも、専用のメソッドを使えば取得・復元できます。

<?php

// 削除済みも含めて全件取得
$allPosts = Post::withTrashed()->get();

// 削除済みのみ取得
$trashedPosts = Post::onlyTrashed()->get();

// 復元する
$post = Post::onlyTrashed()->find(1);
$post->restore();

withTrashed()は「削除済みを含めた全件」、onlyTrashed()は「削除済みのみ」を取得するスコープです。

restore()を呼ぶとdeleted_atがNULLに戻り、通常の取得対象に復帰します。

管理画面に「ゴミ箱」機能を作りたい場合は、onlyTrashed()で一覧を表示し、restore()ボタンを設置するだけで実装できます。

完全に削除したい場合はforceDelete()

「本当に完全削除したい」というケースでは、forceDelete()を使います。

<?php

$post = Post::onlyTrashed()->find(1);
$post->forceDelete(); // deleted_atに関係なく完全に削除される

個人情報保護の観点などで「一定期間が過ぎたソフトデリート済みデータを完全削除する」というバッチ処理を組む場合にも、このforceDelete()が使われます。

まとめ

この記事のポイント

  • SoftDeletesは、レコードを物理削除せずdeleted_atに日時を入れて「削除扱い」にする仕組み
  • マイグレーションでsoftDeletes()、モデルでSoftDeletesトレイトを使うだけで導入できる
  • ユニーク制約と組み合わせる場合、deleted_atを含めた複合ユニーク制約にしないと再登録時にエラーになる
  • withTrashed()onlyTrashed()restore()forceDelete()で削除済みデータを柔軟に扱える

次に読むべき記事

削除処理の安全な扱い方がわかったところで、次はEloquentのデータ加工をより柔軟にする「キャスト・アクセサ・ミューテタ」を見ていきましょう。

→ 次の記事:Eloquentのキャスト・アクセサ・ミューテタの使い分け

タイトルとURLをコピーしました