タイトル: 一意チェック
SEOタイトル: Laravel 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') でも可。