🙌

accepts_nested_attributes_forは本当に非推奨なのか[Rails]

2022/04/18に公開
1

概要

rails accepts_nested_attributes_forでググると非推奨がサジェストされたり、これを使わずに別の方法を取っているような記事が複数ヒットします。

私自身も結構前にコードレビューで「非推奨だよ〜」みたいなコメントをもらったことがあり、その時はそのまま直さなかった気がするのですが、また実装する機会があったので今どういう状況なのか改めて調べてみました。
チーム開発での話なので、個人で使う使わないは自由にしてください。

いつ誰が非推奨と言ったか

非推奨なら普通deprecatedの警告が出ますよね?
非推奨のソースってどこなんでしょう。
↓ここのようです。

https://github.com/rails/rails/pull/26976#discussion_r87855694

dhh on 15 Nov 2016

I'd actually like to kill accepts_nested_attributes_for in due time.
Don't think we should promote it for this new API.
Rather, let's just show how to do it by hand in the controller.

発言者は、dhh。
発言場所は、railsへのPR(helperへのメソッド追加)に対するレビューコメント。
発言した時期は5年以上前。
コメントを日本語にするなら「そのうち消したいと思ってるんだよね」くらいのニュアンスでしょうか。

5年以上前のふわっとしたコメントで、明確な理由や代替案・計画があるというわけでもない。
う〜ん、使うのはやめたほうがいいのか実に微妙です…。

関連する記事

https://discourse.clean-rails.org/t/model-form-1-1/14

4年以上前の記事。
ここでは、よく名前を見るjokerさんやwillnetさんも発言しており、使いにくい部分があることは確かな様ですが、結論を出すまでには至ってません。
この記事の時点では、jokerさんは強く否定派、willnetさんは「わかって使う分にはいいんじゃないのか派」なようです。

https://techracho.bpsinc.jp/hachi8833/2019_11_05/82601

2019年の記事。
accepts_nested_attributes_forに対するコメントがまとめられていますがこのメソッドが憎くてたまらないって雰囲気がでてます。
それだけつまづく人が多いってことなんでしょうね。

Rails7でaccepts_nested_attributes_forのアップデート

delegated_typeへの対応というそれ自体はあまり使わなそうな内容ですが、消すつもりのものをアップデートしますかね?
https://techracho.bpsinc.jp/hachi8833/2022_04_20/117128

結論

使いたくないと考えている人はかなり多いようですが、「非推奨だから使わないほうが良い」と断言するのは言いすぎな気がします。
落とし所としては、チーム内の方針として使わないと決めるのはあり、扱えると判断できるくらいのケースなら使えばいい、といったところでしょうか。

実装してみてコードが複雑そうならやめるとか、チーム内のルールで「使っても良いが○○のようなケースでは複雑になるので使わない」とかバランス良く考えられると一番良さそうです。

つまづきの先(余談)

これはポエムですが、Railsってこういう「シンプルに使えば使いやすいけど、ちょっと複雑なことをやろうとすると面倒くさくなる」ものって結構あると思うんですが、その面倒くさい部分まで含めて時間をかけてちゃんと理解をすると、次回の実装からめちゃくちゃ便利な道具になったりします。

なので、時間がないときにそういうことをする必要はないですが、時間に余裕があるのならあえてつまづきに行ってみるのも面白いと思います。

代替案

代替案についても少し言及してみます。

本当にFormオブジェクトはおすすめできるのか?

代替案としてFormオブジェクトを挙げている記事がたくさんあってびっくりしましたが、残念ながらこれは、使えば問題を簡単に解決してくれる銀の弾丸ではありません。自前で実装する、ということを意味します。

ちなみに、私が実装するならFormオブジェクトではなくてPOROクラス(何も継承しないクラス)として作るのが好みです。
ActiveModelをincludeするとできることが増えて責務が大きくなりがちなので、パラメータ周りの最低限のことだけPOROクラスに任せてバリデーションはデータを持つテーブルのmodelに任せます。

まあ、この辺は代替案というよりはビジネスロジックをどこに置くかの話になってきますね。

合わせて知っておきたいメソッド
autosave: true

親のsaveの際に子のバリデーション、save、transactionをやってくれるhas_one,has_manyのオプションです。
accepts_nested_attributes_forがフロントとの兼ね合いでパラメータ構造が合わないので使えない、という場合なんかにもこれを使う選択肢はありそうです。
ちなみに、accepts_nested_attributes_forは内部的にautosave: trueしています。

詳しい使い方はこの辺の記事を見てください。
参考: https://qiita.com/shyamahira/items/d6592bcfda77c53e19b1

mark_for_destruction

子レコードの削除が必要なケースにmark_for_destructionを使います。(_destroyパラメータを使うことでも削除はできます。)
これは私はaccepts_nested_attributes_forの指定が必要だと思っていたのですが、どうやらautosave: trueすることで使えるようです。

便利ですが、destroyではない方法でレコードを削除するという若干特殊な削除方法にはなりますね。
参考: https://qiita.com/mogulla3/items/010d5b057c00910c085e

accepts_nested_attributes_for実装時の進め方(問題の切り分け方)

accepts_nested_attributes_forでハマる原因として、かなり影響範囲が広いメソッドであるために問題点の切り分けができていない事が多いのではないかと思います。

進め方としては、

  1. viewなしで正しいパラメータ(attributes)が送られれば、正しい値がmodel・子modelにセットされる事を確認する
  2. 正しいattributesがmodelにセットされた後saveした時に正しく更新できることを確認する。
    また、ここまでをrequest specに書いてサーバー側の処理に問題ないことを保証する
  3. fields_forなどのviewの記述を追加した後、ページのhtmlソースを見てパラメータ構造・パラメータ名が正しいことを確認する
  4. railsのdevelopmentログなどで正しいパラメータがsubmitされることを確認する
  5. controllerでmodelに渡しているパラメータが正しいことを確認する

といった具合に進めればどこに問題があるかの切り分けができて進めやすくなるでしょう。
また、web上のコピペできそうなサンプルを動かしてみて、何がそのコードと異なっているのかを比較するというのも良いでしょう。

補足

コメント欄に補足あり

宣伝

こういったRailsの技法・tipsをまとめた本をkindle unlimitedで公開中です。
良かったら読んでみてください。
https://www.amazon.co.jp/dp/B0BNKTNV6M

Discussion

ysi831ysi831

補足

Webの情報では

  • accepts_nested_attributes_forは使うべきでない、今どきはもう使ってない!
  • 代わりにformオブジェクトを使おう!
  • formオブジェクトを使えば複数のモデルを1度に更新できる!

みたいな感じの情報のバイアスがかかっているように思います。
(プログラミングスクールがそういう風に教えている?)
これらは不正確であるためRails初心者がこういった認識を持ってしまうのは悲しいです。

より正確には以下のように順を追って理解する必要があります。

  • accepts_nested_attributes_for を使っているシステムもあるし正しく使えれば便利なので、使う方法・使わない方法どちらもできたほうが良い
    • accepts_nested_attributes_for を使わないとしてもformオブジェクト作成は必須ではない
  • accepts_nested_attributes_for または autosave: trueを使わない場合、以下のような処理をする必要がある
    • それぞれのmodelにattributesをセットする
    • それぞれのmodelにバリデーションをかける
    • transaction内で関連modelを更新するようにする(どちらか片方のmodelだけがエラーになってもrollbackできるようにする)
  • こういった特定の一機能だけの処理はcontrollerやmodelから分離して記述するとfatになることを防げる
  • その方法の1つとしてサービスクラスを運用する方法があり、中でもformオブジェクトというものがある
    • つまりformオブジェクトは処理の置き場所であって、複数のmodelを同時更新するために必ず必要というわけではない
    • formオブジェクトは万能ではなくこれはこれで読みにくくなるケースもある

※私はformオブジェクトを使うよりもこういう感じのもっとシンプルなサービスクラスの実装方法をおすすめしたい(コツ・おまけの部分は除く)
https://qiita.com/QUANON/items/5ef803988c0ad6930e4b#_reference-5ac4a883b01430cd9556

accepts_nested_attributes_forを使う場合・使わない場合の複数model更新処理のイメージ

accepts_nested_attributes_forを使わない場合

# パラメータのセット
model1.assign_assributes(model1_params)
model2.assign_assributes(model2_params)

# バリデーション
if model1.invalid?
  # バリデーションエラー処理
end

if model2.invalid?
  # バリデーションエラー処理
end

# 更新
Model1.transaction do
  model1.save!
  model2.save!
end

accepts_nested_attributes_forを使う場合

# ネストされたパラメータのセット
model1.assign_assributes(nested_params)

# バリデーション(子modelまで同時にかかる)
if model1.invalid?
  # バリデーションエラー処理
end

# 更新(子modelまで同時に更新される)
model1.save!