😊

【Rails】uniqueness: trueだけでは足りない?一意性を確保する方法を見直してみる

に公開

初歩的な話なのですが、個人的にははっとさせられたので忘備録も兼ねて書かせていただきます。
結論としては、マイグレーションファイルにadd_indexの設定もしておきましょうという話です。

一意性を強制するバリデーション設定(ActiveRecordレベル)

モデルのバリデーション設定時に、一意性を強制したいカラム属性に対してuniqueness: trueを設定することができます。

class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 },
                    uniqueness: true
 # ...
end

メールアドレスのような、小文字/大文字の違いを区別しないで一意性を強制したい場合は、以下のようにオプションを渡すこともできます。

uniqueness: { case_sensitive: false }

ここまでで、アプリケーションレベルではemail属性に対して一意性を強制する設定が完了しました。

ただし、これはアプリケーション側での話で、データベースレベルではまだ一意性は保証されていない状態だそうです。
例えば、トラフィックが多い状況で同じユーザーが登録フォームのsubmitボタンを素早く2回押すと、同じメールアドレスを持つ複数のレコードが作成される可能性があるとのことでした。

以下、Railsチュートリアルで紹介されていた事例の抜粋です。

1.アリスはサンプルアプリケーションにユーザー登録します。メールアドレスはalice@wonderland.comです。
2.アリスは誤って"Submit"を素早く2回クリックしてしまいます。そのためリクエストが2件連続で送信されます。
3.次のようなことが順に発生します。リクエスト1は、検証にパスするユーザーをメモリー上に作成します。リクエスト2でも同じことが起きます。リクエスト1のユーザーが保存され、リクエスト2のユーザーも保存されます。
4.この結果、一意性の検証が行われているにもかかかわらず、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。

…完全に理解するにはまだしばらく時間がかかりそうです…。
とにかくデータベースの側でも一意性を設定する必要があるみたいですね。

add_indexを使ってDBレベルでも一意性を強制する

既存のモデルに対して、以下のようにインデックスを追加するマイグレーションを生成します。

rails g migration AddIndexToUsersEmail 

生成されたマイグレーションファイルを以下のように編集します。

class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
 def change
  add_index :users, :email, unique: true
 end
end

マイグレーションを実行するには以下のコマンドを使います。

rails db:migrate

もし新規にモデルを作成する場合は、最初から以下のようにadd_indexを追記しておくこともできます。

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end

    add_index :users, :email, unique: true
  end
end

インデックス追加で一意性を設定する理由

インデックスは、データベースが大量のデータから特定のレコードを素早く見つけるために使用されます。インデックスを設定しないと、データベースに保存されている膨大なレコードから特定のレコード(この場合ではユーザー)を検索する際に「全表スキャン」を行います。これでは、検索やデータ取得に時間がかかってしまいます。

つまり、DBからデータを取得する際に処理が高速化されるということですが、アプリケーションで新しいレコードを追加する際、一意性が求められているカラムがある場合には、同じ値が既に使われていないかどうかを確認するため、DB内のレコードを検索します。このとき、add_indexでインデックスが設定されていると、既存レコードの確認が効率化されます。さらに、unique: trueを使ってインデックスが一意であるよう設定しておけば、DBレベルでも値の重複を防ぐことができるということなんですね。

つまり、DBからデータを取得する際に高速化を図ることができるということですが、アプリケーションが新しいレコードの追加する時に一意性が強制されているカラムがある場合、アプリケーションは同じ値がすでに使われていないかどうかを調べるために、DBに保存されているレコードを検索します。
この時に、add_indexでインデックスが設定されていると、既存のレコードの確認作業が速くなる上、unique: trueでインデックスが一意であるように設定されていると、DBの側でも値が重複がないように設定できるということなんですね。

今回の内容は以上です。
自分の理解に誤りがあればお教えいただけると嬉しいです!
引き続きよろしくお願いいたします。

参考記事

以下記事を参考にさせていただいました。
いつもありがとうございます。
【Rails】 マイグレーションファイルを徹底解説!| Pikawakaさん
https://pikawaka.com/rails/migration#add_index
Railsチュートリアル-第6章ユーザーのモデルを作成する
https://railstutorial.jp/

Discussion