Railsの技: 特定スコープ内でuniquenessバリデーションをかける(翻訳)
データベースでのバリデーションとアプリケーションでのバリデーションを一致させるのはよい考えです。モデルにvalidates :name, presence: true
というバリデーションがあるなら、データベース側にもそれに対応するnot null
制約を付けるべきです。uniquenessバリデーションの場合は、データベースのUNIQUE
インデックスと合わせて使うべきです。
現実のアプリケーションではバリデーションがもっと複雑になりがちですが、それでもできる限りこの方法を続けるべきです。
私の場合、レコードを特定のスコープ内に限って一意にする必要が生じることがよくあります。
たとえば典型的なプロジェクト管理ツールを構築中だとしましょう。Project
に持たせる名前は一意にしてUI画面内で区別できるようにしたいのですが、その名前をグローバルに一意にしたくありません。あるプロジェクトの名前をOnboading
にしたとしても、他の顧客がその名前を使うことに制約を加えるべきではありません。
ありがたいことに、Railsでは「バリデーションスコープ」という便利な機能が使えます。
使い方
Railsのuniquenessバリデーションルールでscope:
オプションを使うと、uniquenessチェックで考慮すべきカラムを追加で指定できます。
class Project < ApplicationRecord
belongs_to :account
has_many :tasks
validates :name, presence: true, uniqueness: { scope: :account_id }
end
このルールは「プロジェクト名は、このアカウントのスコープ内で一意でなければならない」という意味です。言い換えると、name
とaccount_id
の組み合わせが一意である必要がありますが、アカウントが異なれば同じプロジェクト名を使えます。
前述したように、アプリケーションレベルのバリデーションをデータベース制約にも反映したくなるでしょう。
その場合はマルチカラムインデックスを使うことになります。マルチカラムインデックスは以下のように通常のRailsマイグレーションで設定できます。
class CreateProject < ActiveRecord::Migration[6.0]
def change
create_table :projects do |t|
...
end
add_index :projects, [:name, :account_id], unique: true
end
end
オプション
scope:
には複数のカラムを渡せます。
たとえばレストラン向けアプリを構築していて、ゲストが1軒のレストランにつき1日に1回しか予約できないようにしたいとします。
class Reservation < ApplicationRecord
belongs_to :guest
belongs_to :restaurant
validates :guest_id, uniqueness: {
scope: [ :restaurant_id, :reservation_date ]
}
end
デフォルトのエラーメッセージ「{field} has already been taken
」のままではそっけないので、以下のようにエラーメッセージも変更するとよいでしょう。
validates :guest_id, uniqueness: {
scope: [ :restaurant_id, :reservation_date ],
message: "Only one reservation per guest per day is permitted"
}
原注: PostgreSQLの場合はデフォルトのインデックス名の上限が63文字までとなっています。モデルやカラム名が長くなる場合はインデックス名を変更する必要が生じるかもしれません。
add_index :reservations, [:guest_id, :restaurant_id, :reservation_date],
unique: true,
name: "idx_reserveration_guest_date_uniq"
参考資料
- Rails API: Uniqueness Validations
-
PostgreSQLドキュメント: Postgres Constraints
-
MySqlドキュメント: Multi-column Indexes
関連記事
https://techracho.bpsinc.jp/hachi8833/2021_07_15/108763
概要
原著者の許諾を得て翻訳・公開いたします。