LoginSignup
5
3

More than 1 year has passed since last update.

accepts_nested_attributes_for で更新するモデルに対するバリデーションエラー(ActiveModel::NestedError)を外部からセットする方法

Last updated at Posted at 2022-06-02

はじめに

Railsでは accepts_nested_attributes_for を使って、has_many関連する子レコードを1つのフォームで更新することができます。
イメージとしてはこんな感じです。

sample.002.jpeg

上の画面では以下のようなBookモデルと、それに関連するAuthorモデルを更新するフォームです。
(本と著者はmany-to-many関連だろう、というツッコミが来そうですが、説明のために単純化してるので気にしないでください)

class Book < ApplicationRecord
  has_many :authors, dependent: :destroy
  accepts_nested_attributes_for :authors, allow_destroy: true

  validates :title, presence: true
end
class Author < ApplicationRecord
  belongs_to :book

  validates :name, presence: true, uniqueness: { scope: :book_id }
end

また、翻訳情報は以下のように設定されているとします。

config/locales/ja.yml
ja:
  activerecord:
    attributes:
      book/authors:
        name: 著者名
    errors:
      messages:
        blank: を入力してください

このフォームで著者名を空白にして更新すると、必須バリデーションに引っかかってエラーになります。

たとえばrails console上でこの動きをシミュレートすると以下のようなコードになります。

book = Book.find_by(title: 'プログラミング言語C')
author = book.authors[0]
params = {
  title: 'プログラミング言語C',
  authors_attributes: {
    author.id => {
      id: author.id,
      # 著者名をあえて空白にする
      name: '',
      _destroy: "0"
    }
  }
}
book.update(params) #=> false
book.errors.full_messages #=> ["著者名 を入力してください"]

このときbook.errorsの中身は以下のようになっています。

p book.errors
#=> #<ActiveModel::Errors [#<ActiveModel::NestedError attribute=authors.name, type=blank, options={}>]>

ご覧のとおり、ActiveModel::NestedErrorというエラーが入っていて、ここに accepts_nested_attributes_for で更新されるレコードのエラー情報(attribute=authors.name, type=blank)が格納されています。

解決したい問題

ブラウザのフォームからparamsを送信する場合、このエラーはRailsが自動的に設定してくれますが、もし何らかの理由でparamsを使わずにbookauthors.nameに対して開発者が外部から自前でActiveModel::NestedErrorをセットしなければならない場合はどうしたらいいのでしょうか?

book = Book.find_by(title: 'プログラミング言語C')

# 何らかの方法で book に authors.name の ActiveModel::NestedError をセットする

book.errors.full_messages #=> ["著者名 を入力してください"]

ちなみに以下のようなコードではダメでした。

book = Book.find_by(title: 'プログラミング言語C') 

# この書き方だとエラーになる
book.errors.add(:"authors.name", :blank)
#=> NoMethodError: undefined method `authors.name' for #<Book id: 975313824, title: "プログラミング言語C", created_at: "2022-06-02 00:57:31.275997000 +0000", updated_at: "2022-06-02 00:57:31.275997000 +0000">

解決策

以下のように errors.import メソッドを使えば params を使わずに自前で ActiveModel::NestedError をセットできます。

book = Book.find_by(title: 'プログラミング言語C')
author = book.authors[0]

# authors.name の必須エラーを自前でセットする(内部的に ActiveModel::NestedError がセットされる)
book.errors.import(
  ActiveModel::Error.new(author, :name, :blank),
  attribute: :"authors.name"
)

book.errors.full_messages #=> ["著者名 を入力してください"]

errors.importメソッドの実装は以下のようになっています。最後の行でNestedErrorを追加しているのがわかりますね。

# https://github.com/rails/rails/blob/v7.0.3/activemodel/lib/active_model/errors.rb#L129-L136
def import(error, override_options = {})
  [:attribute, :type].each do |key|
    if override_options.key?(key)
      override_options[key] = override_options[key].to_sym
    end
  end
  @errors.append(NestedError.new(@base, error, override_options))
end

(参考)errors.merge! はなんか違う

同じようなメソッドにerrors.merge!があるんですが、これを使うと authors.name ではなく book.name に対するエラーになってしまうので、微妙に動きが異なります。

book = books(:programming_c)
author = book.authors[0]

author.errors.add(:name, :blank)
book.errors.merge!(author)

# book.name の翻訳を取ってこようとして Name になってしまっている
book.errors.full_messages #=> ["Name を入力してください"]

まとめ

単純なバリデーションであればRailsの仕組みをそのまま使えばOKですが、ちょっと変わった要件でモデルの外部から ActiveModel::NestedError をセットしたいという場合はこの方法が使えるかもしれません。

もし他に何かもっといい方法をご存じの方がいたら、コメント欄等で教えてください!

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3