【Rails】親子孫関係にあるテーブルを一度に保存する

はじめに

例えばアンケートを作れるアプリを作成するとします。

その時、アンケートテーブル、質問テーブル、選択肢テーブルを設計したとします。
アンケートには複数個の質問があって、その質問単位に、複数個の選択肢があります。
アンケートと質問がネスト、質問と選択肢がネストして、3階層になっているイメージです。

モデル作成

まず、モデルクラスを作成します。
アンケートは複数の質問を持っている、質問は複数の選択肢を持っている状態をアソシエーションで表します。

# アンケートモデル
class Survey < ApplicationRecord
  has_many :questions
  accepts_nested_attributes_for :questions
end
# 質問モデル
class Question < ApplicationRecord
  belongs_to :survey
  has_many :options
  accepts_nested_attributes_for :options
end
# 選択肢モデル
class Option < ApplicationRecord
  belongs_to :question
end

accepts_nested_attributes_forを定義する

親子関係にあるテーブルを1つのフォームから保存する場合、accepts_nested_attributes_forを定義します。
今回は親子孫まで3代のテーブルを保存したいため、親には子に対して、子には孫に対してのaccepts_nested_attributes_forを定義しています。

コントローラ作成

次に、コントローラを作成します。

class SurveysController < ApplicationController

  def new
    @survey = Survey.new
    questions = @survey.questions.build
    questions.options.build
  end

  def create
    survey = Survey.new(servey_params)
    if survey.valid?
      survey.save!
    else
      render :new
    end
  end

  private
  
  def servey_params
    params.require(:survey).permit(:title, :description, 
      questions_attributes: [:text, :description,
        options_attributes: [:text]
      ]
    )
  end
end

newアクション

新規アンケートの作成を考えて、newアクションを作っていきます。
(設問は自由に増減できることが望ましいですが、今回は親子孫テーブルを同時保存することを目的に書いていますので、1つのアンケートに設問、選択肢は1つずつとします)

この時、親モデルをインスタンス化すると共に、子モデル、孫モデルもインスタンス化しておきます。
でないと、フォームが描画されません。

ストロングパラメータ

モデルにaccepts_nested_attributes_forを定義してあることで、POSTパラメタ(params)がネストして渡ってきます。

そのためストロングパラメータもネストさせます。
子孫モデルのパラメタは_attributesを付けて、子孫モデルの各カラムに紐づくパラメタは配列にします。
子孫モデルは複数個入力される前提なので、paramsも配列で渡ってくるためです。

ビュー作成

いよいよビューを作成します。

<%= form_with model: @survey do |f| %>
  <%= f.label :title %>
  <%= f.text_field :title %>
  <%= f.label :description %>
  <%= f.text_area :description %>
  <%= f.fields_for :questions do |qu| %>
    <%= qu.label :text %>
    <%= qu.text_field :text %>
    <%= qu.label :description %>
    <%= qu.text_area :description %>
    <%= qu.fields_for :options do |op| %>
      <%= op.label :text %>
      <%= op.text_area :text %>
    <% end %>
  <% end %>
  <%= f.submit 'send' %>
<% end %>

fields_for

フォームを書くときに、子孫モデルはfields_forを使って繰り返し描画されるようにします。
その際、親モデルのメソッドとして書くことで、親に紐づいてPOSTされます。

今回は子孫モデルまであるので、孫モデルは子モデルにネストさせて書いています。

最後に

これでsubmitするとcreateアクションに飛んでくるのでストロングパラメータを使ってモデルインスタンスを作り、saveすることで保存されます。