この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:7
ページ更新者:atom
更新日時:2026-06-11 07:07:02

タイトル: 一意チェック
SEOタイトル: Laravel unique バリデーション完全ガイド

この記事の要点
  • Laravel の一意チェックは unique:テーブル,カラム ルールが基本
  • 更新時は自分自身を除外するため unique:users,email,{$user->id} と書く
  • 複雑な条件は Rule::unique()->where() でクロージャ指定
  • ソフトデリート運用なら ->whereNull("deleted_at") を忘れずに
  • バリデーションだけでなく DB 側にも UNIQUE 制約を張って二重に守る

Laravel の unique バリデーションとは

Laravel のバリデータが提供する unique ルールは、指定したテーブル・カラムに同じ値が存在しないことを保証します。ユーザー登録のメールアドレス重複防止などで頻出。

基本的な使い方

// FormRequest または Controller の validate メソッド
$request->validate([
    'email'    => 'required|email|unique:users,email',
    'username' => 'required|string|unique:users,username',
]);

// カラム名が同じなら省略可能
'email' => 'required|email|unique:users',

// 複数の DB ドライバを使う場合は接続名も指定
'email' => 'unique:mysql_legacy.users,email',

更新時に「自分自身」を除外

編集画面で「現在のメールアドレスがそのまま送られても unique エラーにしない」ようにするには、自分の ID を除外指定します:

// 単純な書き方: id カラム = 主キーの場合
$request->validate([
    'email' => 'required|email|unique:users,email,' . $user->id,
]);

// 主キー名が違う場合 (例: user_id)
'email' => 'required|email|unique:users,email,' . $user->user_id . ',user_id',

// Rule クラスでメソッドチェーン (★ 推奨・PHP 8 attribute も使える)
use Illuminate\Validation\Rule;

$request->validate([
    'email' => [
        'required',
        'email',
        Rule::unique('users')->ignore($user->id),
    ],
]);

// 主キー名カスタム
Rule::unique('users')->ignore($user->id, 'user_id'),

// FormRequest 内で $this->user() を使う典型
public function rules(): array
{
    return [
        'email' => [
            'required',
            'email',
            Rule::unique('users')->ignore($this->route('user')->id),
        ],
    ];
}

条件付き unique (where 句)

テナント別・部署別など、特定スコープ内でだけ一意にしたい場合に where を使います:

use Illuminate\Validation\Rule;

// 例: 同じ account_id 内でのみ email が一意
$rules = [
    'email' => [
        'required',
        'email',
        Rule::unique('users')->where(function ($query) use ($accountId) {
            return $query->where('account_id', $accountId);
        }),
    ],
];

// 複数条件
Rule::unique('users')
    ->where('account_id', $accountId)
    ->where('status', 'active'),

// アロー関数 (PHP 7.4+)
Rule::unique('users')->where(fn($q) => $q->where('account_id', $accountId)),

// ignore も併用 (編集時)
Rule::unique('users')
    ->where(fn($q) => $q->where('account_id', $accountId))
    ->ignore($user->id),

ソフトデリート対応

ソフトデリート (deleted_at カラムで論理削除) を使っている場合、削除済みレコードが unique 判定に含まれてしまうと、削除→再登録ができなくなります:

// ❌ 削除済みユーザのメールも重複判定される
'email' => 'unique:users,email',

// ✅ deleted_at IS NULL を条件に追加
Rule::unique('users')->whereNull('deleted_at'),

// FormRequest で
public function rules(): array
{
    return [
        'email' => [
            'required',
            'email',
            Rule::unique('users')
                ->whereNull('deleted_at')
                ->ignore($this->user()->id),
        ],
    ];
}

複合ユニーク制約 (複数カラムの組み合わせ)

「(account_id, email) の組み合わせ」で一意性を担保したい場合、バリデーション側 + DB 側両方で対応します:

// バリデーション (Rule::unique で account_id 条件)
Rule::unique('users')
    ->where('account_id', $request->input('account_id'))
    ->ignore($user?->id),

// マイグレーション (DB 側の UNIQUE 制約)
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('account_id');
    $table->string('email');
    $table->timestamps();
    $table->softDeletes();

    // 複合ユニーク
    $table->unique(['account_id', 'email']);
});

// 後から追加
Schema::table('users', function (Blueprint $table) {
    $table->unique(['account_id', 'email'], 'users_account_email_unique');
});

DB レベルの UNIQUE 制約と組み合わせる

バリデーションだけではレースコンディション (同時送信) で重複が入り込む可能性があります。DB 側にも UNIQUE 制約を張り、二重に守ります:

use Illuminate\Database\QueryException;

try {
    $user = User::create($request->validated());
} catch (QueryException $e) {
    // MySQL の重複エラー番号
    if ($e->errorInfo[1] == 1062) {
        return back()->withErrors([
            'email' => 'このメールアドレスは既に登録されています',
        ])->withInput();
    }
    throw $e;
}

大小文字を区別しない unique

// MySQL は照合順序によって大小区別が変わる
// utf8mb4_unicode_ci → 区別しない (デフォルト)
// utf8mb4_bin        → 区別する

// バリデーション側で正規化
public function prepareForValidation(): void
{
    $this->merge([
        'email' => strtolower($this->email),
    ]);
}

// DB 側で正規化 (mutator)
class User extends Model
{
    protected function email(): Attribute
    {
        return Attribute::make(
            set: fn($value) => strtolower($value),
        );
    }
}

カスタムエラーメッセージ

$request->validate([
    'email' => 'required|email|unique:users,email',
], [
    'email.unique' => 'このメールアドレスは既に登録されています',
]);

// resources/lang/ja/validation.php
'unique' => ':attribute は既に登録されています。',
'attributes' => [
    'email' => 'メールアドレス',
    'username' => 'ユーザー名',
],

テスト

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class RegisterTest extends TestCase
{
    use RefreshDatabase;

    public function test_register_with_duplicate_email_fails(): void
    {
        User::factory()->create(['email' => 'foo@example.com']);

        $response = $this->postJson('/api/register', [
            'name'     => 'Bar',
            'email'    => 'foo@example.com',
            'password' => 'secret123',
        ]);

        $response->assertStatus(422)
                 ->assertJsonValidationErrors(['email']);
    }
}

FAQ

Q: バリデーションでは通るのに INSERT で重複エラーになる
A: バリデーションと INSERT の間にレースコンディションがあります。DB の UNIQUE 制約と QueryException 捕捉、または DB::transaction() + 排他ロックで対応します。

Q: 大量データで unique バリデーションが遅い
A: 該当カラムにインデックスを張ってください。UNIQUE 制約があれば自動的にインデックスが作られます。

Q: 別 DB のテーブルで一意チェックしたい
A: unique:接続名.テーブル,カラム 形式で接続名を指定できます。Rule::unique('users')->connection('other_db') でも可。