はじめに
DBの保存前に重複をチェックする実装を行った記録です。
背景
ApplicationRecordのバリデータで uniqueness: true
を設定した場合
class Account < ApplicationRecord
validates :email, uniqueness: true
end
- オブジェクトが保存される直前に属性の値が一意であり重複していないことをチェックする
- その属性と同じ値を持つ既存のレコードがモデルのテーブルにあるかどうかを調べるSQLクエリを実行することでバリデーションを行う
- つまり、同時に複数の値を保存する場合などはバリデーションが行われない
実装例
- 多対多の関連付けがされているcourseテーブルとuserテーブル、その中間テーブルであるcourse_usersテーブルがあると想定する
class Course < ApplicationRecord
has_many :course_users
accepts_nested_attributes_for :course_users
has_many :users, through: course_users
end
class CourseUser < ApplicationRecord
belongs_to :course
belongs_to :user
end
① カスタムバリデータで実装する
class Course < ApplicationRecord
+ validates_with Validators::UniqueUserValidator
end
+ class UniqueUserValidator < ActiveModel::Validator
+ def validate(record)
+ user_ids = []
+ record.course_users.each do |c|
+ if user_ids.include?(c.user_id)
+ record.errors.add(:base, '重複しています')
+ return
+ end
+ user_ids << c.user_id
+ end
+ end
+ end
② コールバックでバリデーションを実行する
class Course < ApplicationRecord
+ before_validation :ensure_user_is_unique
+ def ensure_user_is_unique
+ ensure_resouce_is_unique(course_users, user_id)
+ end
+ def ensure_resource_is_unique(collection, attribute_name)
+ ret = true
+ uniq = []
+ collection.each{|m|
+ if uniq.include?(m[attribute_name])
+ errors.add(:base, '重複しています')
+ ret = false
+ else
+ uniq << m[attribute_name]
+ end
+ end
+ ret
+ end
end
おわりに
Railsガイドでは下記の通りデータベースに一意性の制約の指定をするように説明しています。一意性制約を指定する際にはApplicationRecordのバリデータのみでは不完全であるということに注意したいと思います。
このバリデーションはデータベースに一意性制約(uniqueness constraint)を作成しないので、異なる2つのデータベース接続が使われていると、一意であるべきカラムに同じ値を持つレコードが2つ作成される可能性があります。これを避けるには、データベース側でそのカラムにuniqueインデックスを作成する必要があります。