Laravel バリデーションでのSoftDelete(論理削除)とUnique制約の最適解

Laravelでのバリデーションチェックはとても便利で使いやすいですが、Unique制約のチェックを行う際に想定と異なる挙動をする使い方があります。

SoftDelete(論理削除)済みのデータがUnique制約チェック対象とならない!

このため、データベースにUniqueキーを設定していると論理削除済みデータと同じデータをInsertしようとするとキー重複でエラーになります。

SQLSTATE[23000]: Integrity constraint violation: 2 Duplicate entry '2' for key 'email_unique'

ググってみましたがピンとくる解決策が見当たらなかったので、対策を考えてみました。

何が起こっているか

事前準備

まずはUserモデルに対してSoftDeleteを設定し、emailにはUnique制約を行います。

マイグレーションファイルを抜粋

Schema::table('Users', function (Blueprint $table) {
            $table->id();
            $table->string('email')->unique();
            $table->softDeletes();
});

User.phpはこちら

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
    use SoftDeletes;
    
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

}

このUserテーブルを作成しデータを準備します。

idemaildeleted_at
1syun@test.com
2taro@test.com2023/6/22
3saburo@test.com

3件のデータが登録され、id:2のtaroさんは論理削除されています。

この状態でLaravelから「taro@test.com」のメールアドレスを検索するとヒットしません。

論理削除されているため自動で無視するようになっています。便利ですね。

バリデーションルール作成

メールアドレスの新規登録や更新時にUnique制約のエラーメッセージが表示できるようにemailに対しバリデーションルールを設定します。

public function rules()
{
   return [
      'email' => 'unique:users'
   ];
}

重複データの登録

これで準備は完了です。

新しく会員を作成するテストを行っていきましょう

ケース1 新規登録

ケース入力値期待動作結果
新規登録goro@test.com登録できること

これは問題なく動作します。

ケース2 重複エラー

ケース入力値期待動作結果
新規登録syun@test.com登録できないこと

正しく重複チェックエラーが表示されることが確認できます。

ケース3 論理削除済みの会員 ← 問題のケース

ケース入力値期待動作結果
新規登録taro@test.com登録できないこと×

エラーになります。

テーブル上には存在しているのですが、バリデーションチェックをパスしSQLのUnique制約違反でエラーになります。

原因

LaravelのモデルにSoftDelete設定を行うと、deleted_atのカラムにデータが入っている場合はそのデータを無かったことのように無視してくれます。

データを参照する際などには問題ないのですが、Unique制約など行っていると上記でご紹介した問題が発生します。

解決策 カスタムルールの作成

論理削除データも含めて自分以外のメールアドレスが重複しているかどうかを検証するバリデーションのカスタムルールを作成しました。

ValidationRuleによるカスタムルール

<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class EmailUnique implements ValidationRule
{
    protected int $user_id;

    public function __construct(int $user_id)
    {
        $this->user_id = $user_id;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {

        $object = User::where('email' , '=' ,  $value)->whereNot('user_id' , $this->user_id ?? null)->first();

        if(empty($object) === false){
            $fail(__('メールアドレスはすでに登録されています。'));
        }

    }

Laravel 9以降は「Rule」の継承が非推奨になっています。
「ValidationRule」を利用しましょう。

カスタムルールの利用方法

カスタムルールの利用方法はこちら。form requestなどでバリデーションします。

自身のメールアドレスを除外するために、ユーザのIDを渡しています。

    public function rules(): array
    {
        return [
            'email' => new EmailUnique(Auth::id())
        ];
    }

これで論理削除済みのデータも含めたUnique制約が確保できました。

まとめ

論理削除でデータを残したままUnique制約をかけるという要件がレアケースかもしれませんが、実現方法の一例として頭の片隅に入れておくと、いつか役に立つかもしれません。

コメントを残す