🤦‍♂️

【Rails】accepts_nested_attributes_forを使用しない方が良い理由とその代替方法

2021/07/03に公開

「Rails 子モデル 保存」 とかでググるとたくさんのaccepts_nested_attributes_forを使用した記事をお見かけすると思います。

そんなaccepts_nested_attributes_forですが、いま自分が働いている会社では使用することを禁じられており、なるべくクリーンな実装を心がけるということを日々意識しております。
そんなこともあり、改めて使用してはいけないものの正体を調べてみました。

accepts_nested_attributes_forとは何か

一言でいえば、「親モデルに紐づくレコードの保存を簡単にできるようにするためのもの」 です。

具体例で見てみましょう。
下記に2つモデルがあり、親モデルであるAuthorの作成と同時にそれに紐づくBookモデルのレコードも作成するというイメージです。

  • Author(著者)
  • Book(本)
# 著者モデル
class Author < ActiveRecord::Base
  has_many :books
  accepts_nested_attributes_for :books
end

# フォームから送信するパラメータ
params = { name: {
  name: '礼流図 太郎', books_attributes: [
    { title: 'おいしいジュースのつくりかた' }
  ]
}}

# コントローラ内で行う処理
def create
  Author.create(params_permitted)
end

def params_permitted
  params.require(:author).permit(:name, books_attributes: [:title])
end

accepts_nested_attributes_for :booksを定義することで、
Authorモデル内にdef books_attributes=(attributes)なセッターメソッドが生え、
フォームから送られてくるパラメータのbooks_attributesによりセッターメソッドが呼ばれることで、Author.create()でBookモデルのレコードも同時に作成されるというカラクリですね。めちゃ便利!

なぜ使用しない方がよいのか

調べたところ、いくつか理由はありそうですが、主に下記の2つの理由からなのではと思いました。

  • ActiveRecordから広げて素のRubyなどでいろいろやりたいってなったときに使用できないため、破綻への道を進む
  • accepts_nested_attributes_for を使った入れ子のformが罠だらけなので、formの実装が泥臭くなりがち

https://qiita.com/shingo-nakanishi/items/229ae20bc1ddf7192dfb

実際に破綻への道を辿ったわけではないので、具体的な悪例は見つかりませんでした。他にもこんな理由で使用しないほうが良いなど知っている方いたら教えていただけると嬉しいです。

ちなみにRails生みの親も「killしたいよ」と言っていたりするで使用しないほうがベターなのでしょう。
https://github.com/rails/rails/pull/26976#discussion_r87855694

使用しないための代替案

Formオブジェクトを活用する

ポイントとしてはFormクラス内で、

  • booksというattributeを定義すること
  • Form内でparamsメソッドを定義して、コントローラではform.paramsでいい感じのパラメータを取得する
    • form.params = { name: "礼流図 太郎", books: { title: "おいしいジュースのつくりかた"} }
  • Authorモデル内でbooksも一緒にビルドしちゃう
# 著者モデル
class Author < ActiveRecord::Base
  has_many :books

  class << self
    def create!(params)
      author = new(name: params[:name])
      author.books.build(params[:books])
      author.save!
      author
    end
  end
end

# フォームから送信するパラメータ
params = { name: {
  name: '礼流図 太郎', books: [{ title: 'おいしいジュースのつくりかた' }]
}}

# コントローラ内で行う処理
def create
  form = AuthorRegistrationForm.new(params_permitted)
  Author.create!(form.params)
end

def params_permitted
  params.require(:author).permit(:name, books: [:title])
end

# Form内
class AuthorRegistrationForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :books

  def params
    attributes.deep_symbolize_keys
  end
end

おわりに

脱accepts_nested_attributes_for!!
ありがとうございました。

Discussion