はじめに
こんにちは。アメリカで独学でエンジニアを目指している者です。
現在 Railsチュートリアルを使用して勉強していますが、RailsのActiveRecord
におけるvalidates
メソッドは、アプリケーションレベルでのデータ検証を行うために非常に有用であることを学びました
しかし、特にuniqueness
バリデーションにおいては、単にvalidates
だけではデータベース上での一意性を完全に保証することができません。
本記事では、その理由と対処法、そしてなぜインデックス(特に一意性インデックス)を用いることで一意性が担保できるのかについて詳しく解説します。
validatesだけではダメな理由
validates :email, uniqueness: { case_sensitive: false }
のようなバリデーションは、レコードを保存する前に現在のデータベースの状態に対してクエリを発行し、同じ値が既に存在しないかをチェックします。
上記のバリデーションは、リクエストごとに生成されたインスタンス上で行われるため、同時に発生する複数のリクエスト間で情報を共有することはできません。
つまり、リクエスト1とリクエスト2がほぼ同時に実行されると、どちらも検証時点では重複がないと判断され、結果として両方のリクエストが成功してしまう可能性があります
対処法
- 一意性インデックスの追加:
現在私が使用しているRailsチュートリアルでは、対象カラムに対して一意性インデックスを設定する手法を採用しています。
これによって、データベースの保存時にデータベース自体が重複を検出しエラーを返すという仕組みになっています。
たとえば、以下のようなマイグレーションファイルを作成します。
class AddIndexToUsersEmail < ActiveRecord::Migration[6.0]
def change
add_index :users, :email, unique: true
end
end
ちなみに一意性インデックスが設定されている場合は以下の処理が行われます
- 1つのリクエストがレコードを挿入しようとすると、データベースはそのトランザクションが完了するまで新しいデータをロックします。
- もし別のリクエストが同じ email を挿入しようとすると、インデックスによるチェックが働き、重複エラー (ActiveRecord::RecordNotUnique) を発生させます。
まとめ
-
validates
のみでは不十分:
ActiveRecord
のvalidates :uniqueness
はアプリケーションレベルでの検証であり、複数リクエスト間の競合状態に対して脆弱です。
そのため、単独ではデータベース上の一意性を完全には保証できません。 -
対処法:
データベースレベルでの一意性制約(一意性インデックス)の追加があげられます -
インデックスによる保証:
一意性インデックスはデータベース内部で重複チェックを行い、トランザクションやロック機構を活用するため、並行リクエストがあっても確実に一意性を担保できます。