Ruby on Rails

【Rails】accepts_nested_attributes_forの使い方(超基本)

今日は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>

もぐくん
もぐくん
めっちゃ便利じゃん!
さかい
さかい
便利だけどモデルの定義とビューの定義が一体化されたり、fields_forの挙動が変わってハマりやすかったりデメリットもあるよ

次にコントローラーを書いていきます。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にはオプションがたくさんあるので、こちらもまたの機会に紹介していこうと思います。
ここまで読んでいただきありがとうございました!

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!