タイトル: 存在チェック
SEOタイトル: Laravel バリデーション exists 完全ガイド
| この記事の要点 |
|
基本: exists ルール
「指定したテーブル・カラムに値が存在すること」を検証します。
$request->validate([
// users テーブルの id カラムに存在
'user_id' => 'required|exists:users,id',
// email カラムに存在(ログイン時のメール存在チェック)
'email' => 'required|email|exists:users,email',
// カラム名を省略するとフィールド名と同じになる
// 下記は 'category_id' = 'exists:categories,category_id' になるので注意
'category_id' => 'required|exists:categories', // 多くは 'exists:categories,id' が意図
// 配列の全要素をチェック
'role_ids' => 'required|array',
'role_ids.*' => 'exists:roles,id',
]);
Rule::exists で条件付き検証
「アクティブなユーザーのみ」「特定の組織に属するレコードのみ」など、追加条件を付けるには Rule::exists() のクロージャを使います:
use Illuminate\Validation\Rule;
$request->validate([
'user_id' => [
'required',
Rule::exists('users', 'id')->where(function ($query) {
$query->where('status', 'active')
->whereNull('deleted_at');
}),
],
// ログインユーザーの会社のメンバーであること
'assignee_id' => [
'required',
Rule::exists('users', 'id')->where(
fn ($q) => $q->where('company_id', auth()->user()->company_id)
),
],
// 別 DB 接続
'external_id' => [
Rule::exists('mysql_legacy.customers', 'cust_id'),
],
]);
unique ルール (重複禁止)
新規登録時の「同じメールアドレスは登録不可」など:
$request->validate([
'email' => 'required|email|unique:users,email',
'username' => 'required|unique:users,username',
]);
編集時の unique: 自分自身を除外
編集画面で「メールアドレスは変更不可だが unique チェックが走ると自分自身に引っかかる」問題。ignore() で除外:
use Illuminate\Validation\Rule;
public function update(Request $request, User $user)
{
$request->validate([
'email' => [
'required', 'email',
// ★ 編集中のユーザー自身は除外
Rule::unique('users')->ignore($user->id),
],
]);
}
// カラム名を id 以外で指定
Rule::unique('users')->ignore($user->uuid, 'uuid');
// ソフトデリート除外
Rule::unique('users')->whereNull('deleted_at')->ignore($user->id);
複合キーでの存在チェック
例: 「(user_id, role_id) の組み合わせが pivot に存在する」「同じ日に同じユーザーで予約が無いこと」
$request->validate([
'date' => [
'required', 'date',
Rule::unique('reservations')
->where(fn ($q) => $q->where('user_id', $request->user_id))
->ignore($reservationId),
],
'role_id' => [
Rule::exists('role_user', 'role_id')
->where(fn ($q) => $q->where('user_id', $request->user_id)),
],
]);
FormRequest にまとめる
Controller が肥大化しないよう、php artisan make:request StoreUserRequest で専用クラスへ:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreUserRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', User::class);
}
public function rules(): array
{
$userId = $this->route('user')?->id;
return [
'name' => 'required|string|max:255',
'email' => [
'required', 'email',
Rule::unique('users')->ignore($userId),
],
'department_id' => [
'required',
Rule::exists('departments', 'id')->where(
fn ($q) => $q->where('active', 1)
),
],
];
}
public function messages(): array
{
return [
'email.unique' => 'このメールアドレスは既に登録されています。',
'department_id.exists' => '指定された部署は存在しないか無効です。',
];
}
}// Controller
public function store(StoreUserRequest $request)
{
// 既にバリデーション済み
$validated = $request->validated();
User::create($validated);
}
カスタムメッセージとフィールド名
$request->validate([
'user_id' => 'required|exists:users,id',
], [
'user_id.exists' => '指定されたユーザーは存在しません。',
'user_id.required' => 'ユーザー ID は必須です。',
], [
'user_id' => 'ユーザー', // :attribute 用の表示名
]);
グローバルメッセージは resources/lang/ja/validation.php:
return [
'exists' => '選択された:attributeは正しくありません。',
'unique' => 'この:attributeは既に使用されています。',
'attributes' => [
'email' => 'メールアドレス',
'user_id' => 'ユーザー',
],
];
パフォーマンス: exists / unique はインデックスを使う
exists:users,email は内部で SELECT count(*) FROM users WHERE email = ? を発行します。email にインデックスが無いと全件スキャンになり、大量データで激重に:
// マイグレーションで明示的にインデックスを
Schema::table('users', function (Blueprint $t) {
$t->index('email');
// unique 制約があれば不要(unique 自体がインデックス)
$t->unique('email');
});
// 確認
DB::select('EXPLAIN SELECT count(*) FROM users WHERE email = ?', ['a@b.c']);
配列で大量チェック時の N+1 問題
下記の exists:roles,id は配列の要素ごとに 1 クエリ発行します:
// ❌ 100 個の role_ids で 100 クエリ
$request->validate([
'role_ids' => 'array',
'role_ids.*' => 'exists:roles,id',
]);
// ✅ カスタムルールで 1 クエリ
use Illuminate\Validation\Rule;
$request->validate([
'role_ids' => ['array', function ($attr, $value, $fail) {
$missing = collect($value)->diff(
DB::table('roles')->whereIn('id', $value)->pluck('id')
);
if ($missing->isNotEmpty()) {
$fail("存在しないロール: " . $missing->implode(','));
}
}],
]);
FAQ
Q: exists でソフトデリート済みも含めたい
A: デフォルトでは生 SQL で WHERE deleted_at IS NULL は付きません(exists ルールはモデルを使わないため)。逆に除外したいなら ->whereNull('deleted_at') を明示。
Q: Rule::exists のクロージャの中で auth() が使える?
A: 使えます。ただしクロージャ実行時の認証状態が反映されるため、ジョブ内で auth()->user() が null になるケースに注意。
Q: 存在しなければ作成、存在すれば更新(upsert)の検証は?
A: exists ではなく required+モデル側の updateOrCreate や upsert を使う設計が一般的です。