多対多の保存処理を実装する
Railsは普段使わないこともあり、いつも、多対多のフォームを作るときに、どうやったけ?となるので、メモ。
今回は、cocoonで実装してみた。
2021/01/23 追記
こちらの記事ではcocoonというgemを使って実装していますが、FormObjectを使っての実装の方をおすすめします。
https://thoughtbot.com/blog/activemodel-form-objects
gemでの実装は早くて簡単ではありますが、メンテされなくなる可能性や、他の人から見たコードの可読性などを考えると、今回のようなケースでの実装はgemに任せてしまうと危うい気がしています。
開発環境
Ruby 2.5.1
Rails 5.1
仕様
- コースとスポットの関係は多対多
walking_course -> walking_course_spots <- walking_spots - コースを保存するときに、ひもづくスポットを選択して、中間テーブルを作成する
- コースの中で、スポットナンバーをつける
まずはrelationを貼る
has_many :walking_course_spots
has_many :walking_spots, through: :walking_course_spots
has_many :walking_course_spots
has_many :walking_courses, through: :walking_course_spots
belongs_to :walking_course
belongs_to :walking_spot
※注意しないといけないのは、relationの順番。throughの前に、関連付けされるテーブルをhas_manyで宣言しないとエラーになる
複数のテーブルにかかる保存は、nest属性をつける
accepts_nested_attributes_for :walking_course_spots, allow_destroy: true
このように記述すれば、courseの保存をしたときに、同時にwalking_courseのテーブルにも保存するようにすることができる
<div class="form-group">
<%= f.fields_for :walking_course_spots do |course_spot| %>
<%= course_spot.collection_checkboxes :walking_course_id, WalkingSpot.all %>
<%- end %>
</div>
フォームで項目を作るときは、fields_forを使うとよい。ここではチェックボックスの例。
パラメーターで取得するときは、以下のように記述。
保存するテーブル: [: カラム名]
という形式
def walking_course_params
params.require(:walking_course).permit(
walking_course_spots_attributes: [:walking_spot_id]
)
end
こんな感じで、使えば、チェックボックスで中間テーブルを作成することは、可能。
ただ、今回は、これをselectで作成できるようにしたい。
Gem Cocoonを使う
色々と、調べてみると、cocoonというgemを使えば簡単にselectのフォームを追加したり、削除したりして中間テーブルに保存するように処理を作ることができるらしいので、やってみた
gem 'cocoon'
//= require cocoon
bundle install
でGemを追加後、application.jsに記述
cocoonでviewを書き換える
<%= f.fields_for :walking_course_spots do |course_spot| %>
<div class="field">
<%= render 'walking_course_spot_fields', f: course_spot %>
</div>
<%- end %>
<div class="links">
<%= link_to_add_association 'スポットを追加', f, :walking_course_spots %>
</div>
<div class="nested-fields field">
<div>スポット</div>
<div class="col-md-6">
<label class="control-label">スポット名</label>
<%= f.collection_select :walking_spot_id, WalkingSpot.all, :id, :name %>
</div>
<div class="col-md-6">
<label class="control-label">スポットNo</label>
<%= f.text_field :spot_number %>
</div>
<%= link_to_remove_association '削除', f, class: 'btn btn-danger' %>
</div>
link_to_add_associationは、フォームの追加ボタンの役割を果たす
link_to_add_remove_associationは削除ボタン
ここで注意しないといけないのは、ファイル名は関連モデル名+_fieldsという名前にしないとエラーになる点である
中間テーブルのレコードを削除できるようにする
保存したいカラム名を追記すること。ここでは、walking_spot_id、spot_number。
また、再保存の際に、画面で削除したはずのものが、テーブルに保存されてしまうという事態が発生した。どうやら、idと_destroyがないとテーブルの削除が機能しないようになっているらしい。最終的には、下記のようになった。
def walking_course_params
params.require(:walking_course).permit(
walking_course_spots_attributes: [:id, :walking_spot_id, :spot_number, :_destroy]
)
end
参照
GitHub - nathanvda/cocoon: Dynamic nested forms using jQuery made easy; works with formtastic, simple_form or default forms
Rails4でフォーム要素を動的に追加したり削除したり〜nested_form | | Scimpr Blog
cocoon を使って、ネストしたフォームを作る - みかづきメモ