Shred IT!!!!

IT全般について試したこと・勉強したことを綴ったり、趣味について語るブログ

Railsで階層化された複数モデルに対応するフォームの作り方

Rails4.1.8 での自分用メモとして記事にしておく。
業務でモデルの階層が深いけど、1画面でフォームを作らなければならなかったので、簡略化した形で記事にまとめる。

記事は2つに分かれていて、 最初に書いたこの記事は Rails 側で行うべき基本的なことをまとめている。

2つ目の記事では、Javascirpt/Coffeescript と連携して動的に追加する部分を記事にまとめている。

jetglass.hatenablog.jp

概要

今回、業務で実際にあったモデルの階層は下記のような感じ。

イベント 1:N イベント詳細 1:N 中間テーブル N:N トピック

要件としてはざっくり下記とします。

  • 1画面で関連する全モデル要素の編集フォームが表示されること
  • 新規作成・編集・削除ができること
  • 1:N の Nは動的にフォームで追加できること

実装

Javascript で動的にフォームを追加できるようにする予定だが、 今回の実装では accepts_nested_attributes_for, fields_for を利用して、 階層化されたモデルを静的にフォームで利用できるところまで。

準備

とりあえず、必要最低限の準備。
Rails の環境を bundler で作ってある前提で、scaffold や ジェネレーター的なもので作りたい環境を準備する。

bundle exec rails g scaffold Event name
bundle exec rails g model EventDetail detail event_id:integer
bundle exec rails g model Topic name
bundle exec rails g model EventDetailTopic event_detail_id:integer topic_id:integer

テーブル構成は以下の通り。

Event EventDetail Topic EventDetailTopic
id id id id
name event_id name event_detail_id
- detail - topic_id

マイグレートして、DB環境を整える。

bundle exec rake db:migrate

モデルに階層の関連付け(accepts_nested_attributes_for)

フォーム上で 1:N で紐づくモデルも編集するのに、モデルで accepts_nested_attributes_for の定義をしておくと、ビューで fields_for を利用して意図した動作にすることができる。
説明は後にして、一旦、定義の方法。

class Event < ActiveRecord::Base
  has_many :event_details
  accepts_nested_attributes_for :event_details
end

class EventDetail < ActiveRecord::Base
  belongs_to :event
  has_many :event_detail_topics
  has_many :topics, through: :event_detail_topics
  accepts_nested_attributes_for :topics
end

class EventDetailTopic < ActiveRecord::Base
  belongs_to :event_detail
  belongs_to :topic
end

class Topic < ActiveRecord::Base
  has_many :event_detail_topics
end

N:N の扱いではポイントがある。
EventDetail で

has_many :topics, through: :event_detail_topics

と定義しているところ。
through 定義により、中間テーブル(EventDetailTopicテーブル)を意識せず Topic テーブルというか Topic モデルを扱えるようにしている。
その上で、 accepts_nested_attributes_for の対象をTopicテーブルにすることで、フォーム上でも中間テーブルを意識しなくて済む。

コントローラーの実装( javascript の動的追加などを一切考慮しない)

build で階層化されたモデルの空オブジェクトを作成するロジックを追加。
理由としてはビューで出てくる fields_for が関連していて、空のフォームを作るのに必要なため。

  # newメソッドの部分のみを記載
  # GET /events/new
  def new
    @event = Event.new
    @event.event_details.build #追加
    @event.event_details.first.topics.build #追加
  end

ビューの実装( fields_for )

fields_for で階層化したモデル(子要素)を指定することで、フォームにそのモデルの要素を作ることができる。
新規作成時の動作としては、コントローラーで build した空のモデルが渡されて、空の入力フォームが作られる。
編集時の動作では、親モデルに紐づいた子モデルが渡されて、フォームに値が設定された状態になる。

_form.html.erb #divタグの中身のみを記載
  <div class="field">
    <%= f.label :name %><br>
    <%= f.text_field :name %>
    <%= f.hidden_field :id %>
    <%= f.fields_for :event_details do |df| %>
      <%= render partial: "event_detail_form", locals: {df: df } %>
    <% end %>
  </div>
_event_detail_form.html.erb
<div class="field">
  <%= df.label :detail %><br>
  <%= df.text_field :detail %>
  <%= df.hidden_field :id %>
  <%= df.fields_for :topics do |tf| %>
    <%= render partial: "topic_form", locals: {tf: tf } %>
  <% end %>
</div>
_topic_form.html.erb
<div class="field">
  <%= tf.label :topicname %><br>
  <%= tf.text_field :name %>
  <%= tf.hidden_field :id %>
</div>

画面表示

現時点での新規作成画面を表示。
f:id:jetglass:20150410184538p:plain

仮に適当な値を入れて、「Create Event」ボタンを押したときに受け取るパラメータを確認。

[1] pry(#<EventsController>)> params
=> {"utf8"=>"",
"authenticity_token"=>"CXuJ8dX6dILlw5SyTvipjFY6MvHPZooG1DF4rhnEbLc=",
"event"=>{
   "name"=>"n",
   "id"=>"",
   "event_details_attributes"=>{ 
     "0"=>{ 
       "detail"=>"d",
       "id"=>"",
       "topics_attributes"=>{ 
         "0"=>{
           "name"=>"t",
           "id"=>""
         }
       }
     }
   }
},
"commit"=>"Create Event",
"action"=>"create",
"controller"=>"events"}

accepts_nested_attributes_for, fields_for で定義したところが、hoge_attributes としてパラメーターが飛んでくる様子が確認できました!
hoge_attributes の中に "0" というキーが指定されていることに注目。
子要素のフォームが増えてもその値がインクリメントされて、複数の子要素に対応できる。
つまり、1:N の Nに対応している。

コントローラーの修正

コントローラーでパラメーターのホワイトリスト処理的(event_paramsメソッド)なものがあって、 hoge_attributes が受け取れない状態なので修正。
このままだと hoge_attributes は無視されるので、Eventテーブルにレコードが新規追加されるだけになる。

    # 修正前
    def event_params
      params.require(:event).permit(:name)
    end
    # 修正後
    def event_params
      params.require(:event).permit(
        :id,
        :name,
        event_details_attributes: [
          :id,
          :detail,
          topics_attributes: [:id, :name]
        ]
      )
    end

新規作成画面の挙動

上記修正後、新規作成画面のフォームに適当に値を入力して「Create Event」ボタンを押すと、各モデルのリレーションを保った状態で値がDBに保存される。

新規作成ではコントローラーの create メソッドが呼ばれるので、ソースを見てみる。

def create
  @event = Event.new(event_params)
  ...
    if @event.save
   ...

ここで注目すべきは、 event_params には hoge_attributes が含まれているにも関わらず、 Event.new でよしなに関連するモデルを生成してくれること(もちろんリレーションされた状態)。
save メソッドを呼んで保存する際には、関連するモデルも一緒に保存される。

一覧画面

いくつか新規登録した後の一覧画面。
f:id:jetglass:20150415112637p:plain

編集画面の挙動

一覧画面から3つ目の Edit を押して、編集画面へ。
f:id:jetglass:20150415112732p:plain

今度は編集画面で値を編集する。
適当に値を書き換えて「Update Event」ボタンを押下。

編集(更新)ではコントローラーの update メソッドが呼ばれるので、ソースを見てみる。

  def update
    ...
      if @event.update(event_params)
     ...

update メソッドの引数として event_params(hoge_attributes を含む)を渡しても、 よしなに該当するレコードを更新してくれる。

なぜなら下記のように hidden で設定しておいた id が活用されるため。

pry(#<EventsController>)> event_params
=> {
 "id"=>"3",
 "name"=>"ab",
 "event_details_attributes"=>{
   "0"=>{
     "id"=>"3", "detail"=>"ab",
     "topics_attributes"=>{
       "0"=>{
         "id"=>"3", "name"=>"ab"
       }
}}}}

今回はここまで。
accepts_nested_attributes_for, fields_for を利用して、静的に関連するモデルを編集できるフォームを作った。
次回は動的に子要素にあたるモデルの編集フォームを追加できるようにする。

↓続きはこちら

jetglass.hatenablog.jp

参考

Railsでaccepts_nested_attributes_forとfields_forを使ってhas_many関連の子レコードを作成/更新するフォームを作成 - Rails Webook