今日はaccepts_nested_attributes_for
についてかんたんにまとめていこうと思います。accepts_nested_attributes_for
は1つのモデルから複数テーブルにデータを保存するのに使います。便利ですが、DHH氏がコメントで『新しいAPIとして推奨すべきでない』といっているようにあまり推奨はされていないようで、使うかどうかは経験豊富な開発者でも意見が分かれるようです。
導入方法
今回もscaffoldでサクッとアプリケーションをつくっていきます。記事の投稿ができて、それにカテゴリーを登録できます。今回は便宜上、airticleとcategoryは1対多の関係とします。
# action-mailerなどは使わないためスキップ
$ rails new accepts_nested_attributes_for-demo --skip-action-mailer --skip-action-mailbox --skip-action-text --skip-active-storage --skip-action-cable
$ rails g scaffold article title body
$ rails g scaffold category name article_id:integer
$ rails db:create
$ rails db:migrate
accepts_nested_attributes_forは少し特殊な記述をします。まずはモデルから書いていきます。
class Article < ApplicationRecord
has_many :categories
accepts_nested_attributes_for :categories
end
class Category < ApplicationRecord
belongs_to :article
end
この1行を加えるだけで、複数テーブルへの同時保存が可能になりました。
params = { title: "タイトル", body: "記事中身", categories_attributes: [{name: "カテゴリー"}] }
=> {:title=>"タイトル", :body=>"記事中身", :categories_attributes=>[{:name=>"カテゴリー"}]}
Article.create(params)
(0.7ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
Article Create (1.6ms) INSERT INTO "articles" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "タイトル"], ["body", "記事中身"], ["created_at", "2021-04-05 22:51:43.361591"], ["updated_at", "2021-04-05 22:51:43.361591"]]
Category Create (0.2ms) INSERT INTO "categories" ("name", "article_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "カテゴリー"], ["article_id", 3], ["created_at", "2021-04-05 22:51:43.365218"], ["updated_at", "2021-04-05 22:51:43.365218"]]
TRANSACTION (1.5ms) commit transaction
=> #<Article:0x00007fd71035c9d0 id: 3, title: "タイトル", body: "記事中身", created_at: Mon, 05 Apr 2021 22:51:43.361591000 UTC +00:00, updated_at: Mon, 05 Apr 2021 22:51:43.361591000 UTC +00:00>
次にコントローラーを書いていきます。newメソッドにariticleと紐付けたcategoryのインスタンスをビルドする必要があるのと、fields_forで渡ってくるcategories_attributes~~を受け取る記述を足します。
class ArticlesController < ApplicationController
# 略
def new
@article = Article.new
@article.categories.build # 追記
end
# 略
private
def article_params
params.require(:article).permit(:title, :body, categories_attributes: [:name]) # 追記
end
end
最後にビューを書きます。今回はscaffoldで作られたnew.html.erbを少し変えました。
<h1>New Article</h1>
<%= form_with model: @article do |f| %>
<%= f.label :title, "タイトル" %>
<%= f.text_field :title %>
<%= f.label :body, "記事" %>
<%= f.text_field :body %>
<%= f.fields_for :categories do |g| %>
<%= g.label :name, "カテゴリー" %>
<%= g.text_field :name %>
<% end %>
<p><%= f.submit %></p>
<% end %>
<%= link_to 'Back', articles_path %>
ポイントはfields_forを使うところです。このように記述することでコンソールで見たような形のparamsを作ることができます。上で書いたビューをブラウザで表示させた結果が下図です。fields_forでつくったフォームのname属性がarticle[categories_attributes][0][name]
となっていて、この形から{:article => :categories_attributes => [:name => "hoge"]}
というparamsが送られることが何となく分かるかと思います。
実際に送られるparamsが下になります。
params
=> #<ActionController::Parameters {"authenticity_token"=>"ZyN2Q1PwhT-WOX***", "article"=>{"title"=>"タイトル", "body"=>"今日の朝、~~", "categories_attributes"=>{"0"=>{"name"=>"カテゴリー"}}}, "commit"=>"Create Article", "controller"=>"articles", "action"=>"create"} permitted: false>
では、実際に投稿できるか試してみます。本当はカテゴリーを複数登録できるようフォームを変えたほうがですが、ご容赦ください。
フォームを入力して、Create Airticleボタンを押すと、投稿できていることを確認できたと思います。(カテゴリーを表示できるよう少しビューを変えています。)
まとめ
いかがだったでしょうか。すごく簡単に複数テーブルへの同時保存を実装することができることが分かったと思います。accepts_nested_attributes_for
にはオプションがたくさんあるので、こちらもまたの機会に紹介していこうと思います。
ここまで読んでいただきありがとうございました!