Railsで階層化された複数モデルに対応するフォームの作り方
Rails4.1.8 での自分用メモとして記事にしておく。
業務でモデルの階層が深いけど、1画面でフォームを作らなければならなかったので、簡略化した形で記事にまとめる。
記事は2つに分かれていて、 最初に書いたこの記事は Rails 側で行うべき基本的なことをまとめている。
2つ目の記事では、Javascirpt/Coffeescript と連携して動的に追加する部分を記事にまとめている。
概要
今回、業務で実際にあったモデルの階層は下記のような感じ。
イベント 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>
画面表示
現時点での新規作成画面を表示。
仮に適当な値を入れて、「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 メソッドを呼んで保存する際には、関連するモデルも一緒に保存される。
一覧画面
いくつか新規登録した後の一覧画面。
編集画面の挙動
一覧画面から3つ目の Edit を押して、編集画面へ。
今度は編集画面で値を編集する。
適当に値を書き換えて「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 を利用して、静的に関連するモデルを編集できるフォームを作った。
次回は動的に子要素にあたるモデルの編集フォームを追加できるようにする。
↓続きはこちら
参考
Railsでaccepts_nested_attributes_forとfields_forを使ってhas_many関連の子レコードを作成/更新するフォームを作成 - Rails Webook