業務でRailsのテーブル実装をしていたんですが、ユニーク制約をマイグレーションファイルとモデルファイルのどちらに記述すれば良いか迷ってしまいました。
そこでこの記事では実務で得た知見をもとに、Railsアプリケーションにおいてユニーク制約をマイグレーションファイルとモデルファイルのどちらに書くべきかまとめます。
ユニーク制約はマイグレーションファイルとモデルファイルの両方に書く
結論として、ユニーク制約はマイグレーションファイルとモデルファイルの両方に書くべきです。
それぞれ以下のように記述します。
モデルファイルへの記述例
class JobKeyword < ActiveRecord::Base
validates :name, presence: true, uniqueness: true # 一意制約を付与
validates :enabled, presence: true, inclusion: [true, false]
end
マイグレーションファイルへの記述例
class CreateJobKeywords < ActiveRecord::Migration[6.1]
def change
create_table :job_keywords, comment: '仕事のキーワード' do |t|
t.string :name, null: false, comment: 'キーワード名'
t.boolean :enabled, null: false, comment: '有効/無効'
t.text :note, null: true, comment: '備考'
t.timestamps
end
add_index :job_keywords, :name, unique: true # 一意制約を付与
end
end
片方だけだとなぜダメなのか?
当初のぼくは、マイグレーションファイルかモデルファイルのどちらかにユニーク制約をかければ良いと思っていましたが、それだと以下の理由でNGなようです。
モデルファイルだけにユニーク制約を書いた場合
「モデルファイルだけにユニーク制約を書く = アプリケーションレベルでの制約のみ設ける」ということになります。これは以下の理由でNGです。
- 全く同じ時間に登録されたらデータの重複を防げない
- モデルファイルのバリデーションはスキップできてしまう(
save
メソッドにvalidate: false
を引数として与える) - DB移行時に新たなDB側で一位性を保証する追加の作業が必要となる
「全く同じ時間に登録されるなんて実際ないでしょ」と昔のぼくは思っていましたが、大量にアクセスがあるサービスでは、そういったことも起こりうるなと実務を通じて感じています。
マイグレーションファイルだけにユニーク制約を書いた場合
「マイグレーションファイルだけにユニーク制約を書く = データベースレベルでの制約のみ設ける」ということになります。これは以下の理由でNGです。
- データがデータベースに送信されてからでないと重複を防げない(モデルファイルにバリデーションがあればアプリケーションレベルで重複を防げる)
- 条件付きバリデーションが実装できない(「下書き保存」ではバリデーションをスキップするけど「公開」ではバリデーションチェックをしたい、など)
これも昔のぼくは「データベースに送信されるときに防げるならそれでいいじゃん」と思っていました。しかし、アクセス数が膨大なサービスではデータベースへの負荷も考慮する必要があると理解できました。また条件付きのバリデーションも結構出てくる場面があるので、やはりマイグレーションファイルだけにユニーク制約をかけるのも良くないなと腹落ちしました。
まとめ
上記の理由により、モデルファイルとマイグレーションファイルの両方にユニーク制約を設定することで、堅牢かつ柔軟なバリデーションを実現できると学びました。
参考文献
- https://railsguides.jp/active_record_validations.html#uniqueness
- https://railsguides.jp/active_record_migrations.html#active-record%E3%81%A8%E5%8F%82%E7%85%A7%E6%95%B4%E5%90%88%E6%80%A7
- https://railstutorial.jp/chapters/modeling_users?version=6.0#cha-modeling_users
コメント