Posted at

DB保存前のhas_many内で値がユニークか判定をする

More than 3 years have passed since last update.


テーブルに格納されていない値にバリデーションが行われない

ActiveRecord::Base#validatesのuniquenessオプションはテーブルに格納されている値でユニーク判定を行います。

そのためDBに保存前のモデルにはユニーク判定が行われません。

以下サンプルコードです。uriが同じ「a」ですがバリデーションエラーは起こりません。

class Page < ActiveRecord::Base

belongs_to :site
validates :uri, uniqueness: true, scope: {[:site_id, :uri]}
end

class Site < ActiveRecord::Base
has_many :pages
end

site = Site.new
site.pages << [Page.new(uri: :a), Page.new(uri: :a)]
site.save! # ここでエラーにならない

テーブルに格納されていない値にもバリデーションが行われて欲しいですよね。


before_validation に値がユニークかの検証を追加する

これを解決するには親に当たるモデル、この文中の例ではSiteクラスで子に当たるpagesの値を確認します。

まずSite#pages内で値の重複を確認して、問題なければPageモデルのvalidatesでテーブルの値を確認する流れになります。

class Page < ActiveRecord::Base

belongs_to :site
validates :uri, uniqueness: true, scope: {[:site_id, :uri]}
end

class Site < ActiveRecord::Base
has_many :pages
before_validation :children_be_uniq

def children_be_uniq
resource_be_uniq(pages, :uri)
end

def resource_be_uniq(collection, attribute_name)
success = true
uniq = []
collection.each{|m|
if uniq.include?(m[attribute_name])
error_message = I18n.t("errors.messages.taken", attribute: I18n.t("activerecord.attributes.#{m.class.to_s.underscore}.#{attribute_name}"))
errors.add(:base, error_message)
m.errors.add(attribute_name, error_message)
success = false
else
uniq << m[attribute_name]
end
}
success
end
end

site = Site.new
site.pages << [Page.new(uri: :a), Page.new(uri: :a)]
site.save! # ここでエラーになる

きっちりとバリデーションエラーが発生して良い感じですね。

ActiveRecord::RecordInvalid: バリデーションに失敗しました。 URLはすでに存在します。

accepts_nested_attributes_forを使って複数のhas_manyを一括で更新する場合などでも利用できます。

class Site < ActiveRecord::Base

has_many :pages
accepts_nested_attributes_for :keywords, allow_destroy: true
before_validation :children_be_uniq
...以下略
end

permit_attrs = [
{pages_attributes: [:id, :uri, :_destroy]}
]
permit_params = params.require(Site).permit(permit_attrs)
site.update_attributes!(permit_params) # before_validationで値の確認が行われる

質問などあればコメント欄かTwitter宛にお願いします。


リファレンス情報