Laravel withTrashed()の使い方|論理削除データの取得とonlyTrashed()との違い

基本文法・構文ガイド

withTrashed()は、論理削除(ソフトデリート)されたレコードも含めてデータベースから取得するLaravelのEloquentメソッドです。削除済みのレコードだけをピンポイントで取り出したい場合はonlyTrashed()を使い、元に戻すときはrestore()、物理的に抹消するときはforceDelete()を使用します。

この記事では、論理削除の基本から、withTrashed()onlyTrashed()の使い分け、実務で頻出するUnique制約との衝突回避バリデーション管理画面での動的切り替え検索リレーション先(1対多・多対多・belongsTo)での論理削除の扱い、Laravelの自動パージ機能(Prunableトレイト)、複合インデックスによるパフォーマンス最適化までを体系的に解説します。

ソフトデリートの基本概念やテーブルマイグレーション、設計上のメリットを全体的に確認したい方は、ハブ記事のLaravelでの論理削除を理解する:実装方法とメリットを徹底解説もあわせてご覧ください。

ソフトデリートの仕組みと前提

Laravelのソフトデリート(論理削除)は、レコードを物理的に削除せず、deleted_atという日時カラムを更新して「削除された状態」にする仕組みです。モデルにSoftDeletesトレイトを設定すると、通常の検索クエリにWHERE deleted_at IS NULLが自動付加され、削除済みレコードは検索対象から除外されます。

ステップ1: モデルとテーブルの準備

モデルにuse SoftDeletes;を追加し、マイグレーションで$table->softDeletes();を実行しておくことで、ソフトデリートが有効になります。

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

class Post extends Model
{
    use SoftDeletes;

    protected $fillable = ['title', 'body', 'status'];
}

マイグレーション例:

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->string('status')->default('draft');
    $table->timestamps();
    $table->softDeletes(); // deleted_at (TIMESTAMP nullable) を作成
});

ステップ2: ソフトデリートの実行・復元・判定

論理削除を行うには、通常の削除と同様にdelete()メソッドを呼び出します。これによりdeleted_atに現在時刻が記録されます。

// ソフトデリートの実行
$post = Post::find(1);
$post->delete();

// レコードが論理削除状態か判定(bool)
if ($post->trashed()) {
    // 削除済みの場合の処理
}

// レコードの復元(restore)
$deletedPost = Post::withTrashed()->find(1);
$deletedPost->restore(); // deleted_at が null に戻る

削除処理を安全に行うためのトランザクション設計や物理削除との使い分けについては、Laravelでの安全な削除操作:deleteメソッドの使い方と注意点で詳しく紹介しています。

ステップ3: withTrashed() と onlyTrashed() の使い分け

論理削除されたレコードを取得するためのメソッドには、用途に応じてwithTrashed()onlyTrashed()の2つがあります。

メソッド 取得対象 付加されるSQL条件 主なユースケース
通常クエリ(指定なし) 有効なレコードのみ WHERE deleted_at IS NULL 一般ユーザー向け一覧画面・詳細画面
withTrashed() 有効+削除済みの全件 (条件解除: deleted_atのWHEREなし) 全件監査ログ、注文履歴の過去データ参照
onlyTrashed() 削除済みレコードのみ WHERE deleted_at IS NOT NULL 管理画面の「ゴミ箱」「復元待ち一覧」

withTrashed() の使い方

管理画面や管理用バッチなどで、削除済みデータを含めたすべてのレコードを取得したい場合に使用します。

// 全ての投稿を取得(削除済みを含む)
$allPosts = Post::withTrashed()->get();

// 削除済みも含めて特定のIDを検索
$post = Post::withTrashed()->find($id);

// 削除済みも含めてページネーション
$posts = Post::withTrashed()->paginate(20);

onlyTrashed() の使い方

「ゴミ箱」画面のように、削除されたデータだけを一覧表示・検索したい場合に使用します。

// 削除された投稿のみを取得
$trashPosts = Post::onlyTrashed()->get();

// 削除済みレコードの中から特定の条件で絞り込み
$inactiveTrash = Post::onlyTrashed()
    ->where('status', 'draft')
    ->get();

実務で役立つ実践パターンとコード例

1. 会員退会(論理削除)と再登録時のUniqueバリデーション

実務で非常によくある課題が「退会したユーザーのメールアドレスで再登録できるようにしたい(またはできないようにしたい)」というケースです。Rule::unique()whereNull('deleted_at')を組み合わせることで、「有効な会員の中でのみ一意」というルールを正確に定義できます。

use Illuminate\Validation\Rule;

public function rules(): array
{
    return [
        // 論理削除されたユーザーは除外し、有効なユーザー間でのみメールアドレスの重複をチェック
        'email' => [
            'required',
            'email',
            Rule::unique('users', 'email')->whereNull('deleted_at'),
        ],
    ];
}

2. 管理画面での一覧フィルタリング(全件 / 有効 / ゴミ箱 を動的切り替え)

管理画面の一覧画面で、ユーザーが「すべて」「有効」「ゴミ箱」をセレクトボックス等で切り替えるクエリは、when()メソッドを使ってエレガントに記述できます。

use App\Models\Post;
use Illuminate\Http\Request;

public function index(Request $request)
{
    $trashedFilter = $request->input('trashed', 'active'); // 'all', 'only', 'active'

    $posts = Post::query()
        ->when($trashedFilter === 'all', fn ($query) => $query->withTrashed())
        ->when($trashedFilter === 'only', fn ($query) => $query->onlyTrashed())
        ->latest()
        ->paginate(20);

    return view('admin.posts.index', compact('posts', 'trashedFilter'));
}

動的クエリの構築手法については、Laravelのwhenメソッドの使い方|条件分岐をスマートに書くテクニックもご覧ください。

3. リレーションでの withTrashed() の活用と定義への組み込み

Eager Loading(with)で関連モデルを取得する際、関連先がソフトデリートされていても一緒に取得したい場合はクロージャ内でwithTrashed()を指定します。

// ユーザー一覧を取得し、削除済みの投稿も含めてEager Loading
$users = User::with(['posts' => function ($query) {
    $query->withTrashed();
}])->get();

// 逆方向:注文に紐づくユーザーが論理削除されていても取得する
$orders = Order::with(['user' => function ($query) {
    $query->withTrashed();
}])->get();

さらに、「注文データからは常に退会済みユーザーも含めて取得したい」という場合は、モデルのリレーション定義側に直接withTrashed()を記述しておくことも可能です。

class Order extends Model
{
    /**
     * 注文したユーザー(退会済みユーザーも含めて常時参照可能にする)
     */
    public function user()
    {
        return $this->belongsTo(User::class)->withTrashed();
    }
}

4. 除外条件クエリ(whereNot / whereNotIn)との組み合わせ

「削除済みデータも含めて取得しつつ、特定のステータスやカテゴリを除外したい」場合は、whereNot()whereNotIn()と組み合わせます。

// 削除済みを含む全件から、statusが'spam'以外の投稿を取得
$validPosts = Post::withTrashed()
    ->whereNot('status', 'spam')
    ->get();

否定条件や除外条件の書き方については、Laravel whereNot/whereNotInの使い方と違い|除外条件の書き方と実践例もあわせてご覧ください。

ステップ4: ソフトデリートされたデータの完全削除(forceDelete)と自動パージ(Prunable)

論理削除されたデータをデータベースから物理的に完全消去するには、forceDelete()メソッドを使用します。

// ゴミ箱にある特定のレコードを完全削除
$trashPost = Post::onlyTrashed()->find($id);
if ($trashPost) {
    $trashPost->forceDelete(); // DELETE FROM posts WHERE id = ? が実行される
}

Prunable トレイトによる期限切れデータの自動パージ

Laravel標準のPrunable(またはMassPrunable)トレイトを利用すると、「削除後30日を経過した古い論理削除レコード」を定期コマンド(php artisan model:prune)で安全かつ自動的に物理消去できます。

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

class Post extends Model
{
    use SoftDeletes, Prunable;

    /**
     * 自動削除(パージ)の対象クエリを定義
     */
    public function prunable(): Builder
    {
        // 削除されてから30日以上経過したレコードを対象にする
        return static::onlyTrashed()->where('deleted_at', '<=', now()->subDays(30));
    }
}

パフォーマンスとインデックス設計のベストプラクティス

ソフトデリートを導入すると、すべてのクエリにWHERE deleted_at IS NULLが付加されます。数百万件規模のテーブルでは、インデックス設計が検索性能を大きく左右します。

  1. 複合インデックスの作成: 検索によく使われるカラムとdeleted_atを組み合わせた複合インデックス(例: [user_id, deleted_at][status, deleted_at])を作成します。
  2. 部分インデックス(PostgreSQL等の場合): WHERE deleted_at IS NULLのレコードのみを対象とする部分インデックスを作成すると、インデックスサイズを大幅に削減できます。

レコードの存在判定を高速に行いたい場合は、Laravel Eloquent Existsメソッドの使い方とパフォーマンス向上の秘訣も参考にしてください。

よくある疑問とその解決策

Q. trashed() メソッドは何をするものですか?

モデルインスタンスが現在論理削除されているかどうかを真偽値(bool)で判定するメソッドです。$post->trashed()trueなら削除済み、falseなら有効な状態です。

Q. クエリビルダ(DB::table)でも withTrashed() は使えますか?

使えません。withTrashed()onlyTrashed()はEloquent ORMの機能です。クエリビルダを使用する場合は、whereNull('deleted_at')whereNotNull('deleted_at')を手動で指定する必要があります。

Q. グローバルスコープとSoftDeletesの兼ね合いはどうなりますか?

SoftDeletesはLaravel内部でSoftDeletingScopeというグローバルスコープとして実装されています。withoutGlobalScopes()を実行すると、SoftDeletingScopeも含めてすべてのグローバルスコープが解除されるため、論理削除データも取得される点に注意してください。

まとめ

Laravelのソフトデリートでは、withTrashed()onlyTrashed()を使い分けることで、削除済みデータの参照や管理画面のゴミ箱機能をシンプルかつ安全に構築できます。実務ではUniqueバリデーションでのwhereNull併用リレーション定義への組み込みPrunableトレイトによる自動定期パージをセットで押さえておくことが重要です。

論理削除の全体像や設計パターンを深く理解したい方は、ハブ記事のLaravelでの論理削除を理解する:実装方法とメリットを徹底解説もぜひチェックしてみてください。

レン (Wren)

こんにちは。レンです。

Laravelのコードの森に住んでいる、小さな案内役です。
ルーティングの枝やクラスの影を歩きながら、コードの流れや仕組みを眺めています。

このサイトでは、Laravelの基本から実装のコツまで、開発で役立つポイントを静かに整理しています。
難しいことを増やすのではなく、コードの見通しが少し良くなるヒントを届けるのが役目です。

「この処理はどこに書くのがいいのか」
「Laravelではどう考えると整理できるのか」

そんな疑問に、小さなメモを残すような気持ちで記事を書いています。

コードを書いている途中で迷ったとき、
このサイトが少し立ち止まって整理できる場所になればうれしいです。

レン (Wren)をフォローする

コメント