中間テーブルで扱うデータがpolymorphic
になっていて、それを該当モデルからthrough
を使って呼び出すまでは簡単だったんだけれど、それをView側で更新するのが大変だったのでメモとして書いておく。
has_manyでpolymorphicな関連を取得
まずはpolymorphicなモデルを扱う中間テーブルのモデル。
class Combination < ApplicationRecord
belongs_to :group, polymorphic: true
belongs_to :group_item, polymorphic: true
validates :group_item, presence: true
validates :group, presence: true
validates_uniqueness_of :group_id, scope: [:group_type, :group_item_id, :group_item_type] end
end
次に、Memoモデル。
メモを確認できるメンバーを、中間テーブルcombinations
を通じて取得する。
has_manyを2つ定義している。
class Memo < ApplicationRecord
has_many :combinations, as: :group, dependent: :destroy, inverse_of: :group
has_many :members,
through: :combinations,
source: :group_item,
source_type: 'User',
autosave: false
accepts_nested_attributes_for :members
end
本来group_item
はpolymorphicなので、どんなモデルでも入り得るのだが、source_typeをUser
にすることで、User
のインスタンスとして取得できるようにしてある。
次にUserモデル。一応載せていますが、今回の主役はMemoモデルです。
class User < ApplicationRecord
has_many :combinations, as: :group_item, dependent: :destroy
has_many :memos, inverse_of: :user, dependent: :destroy
end
こうしておくと、メモが見られるメンバーをすぐ取得できます(メモが見られる、見られないの実装は省略)
memo = Memo.first
memo.members # メモが見られるメンバーを取得
View側の実装
View側では、fields_for
を使って実装していきます。
肝になるのは、include_id: false, multiple: true
の部分と、fm.check_box
のinclude_hidden: false
です。
<%= simple_form_for @memo do |f| %>
<%= f.input :title %>
<%= f.input :content %>
<%= f.fields_for :members, User.all, include_id: false, multiple: true do |fm| %>
<label>
<%= fm.check_box :id,
{
include_hidden: false,
checked: @memo.member_ids.include?(fm.object.id)
},
fm.object.id %>
<%= fm.object.name %>
</label>
<% end %>
<%= f.button :submit %>
<% end %>
include_id: false
fields_forでinclude_id: false
を指定しない場合、デフォルトでidがhidden
フィールドが自動的に追加されてしまい、必ず送られてしまいます。その結果、チェックボックスに関係なく、全員がメモを見られるメンバーになってしまいます。ですので、自動的にidのhiddenフィールドが作られないようにします。
include_hidden: false
check_boxは、デフォルトでチェックが入っている時の値と、入っていないときの値を送ることができます。
入ってない場合の値はhidden
フィールドに設定されます。
今回のパターンではメンバーになるUserのidを送りたいのですが、チェックが入ってない場合は何も送る必要がないので、hiddenフィールド自体を出さないようにします。
Controllerの実装
controller側では、strong parametersの実装で、membersを値が渡ってくることを許可します。
追記(2019-02-12)
更新の際の動作がうまく動いていませんでした。controllerに修正が必要でした。
具体的には、members_attributesに該当するチェックボックスを全て外した状態で更新しようとすると、members_attributesが送られてこないため、あとで説明するmembers_attributes=
メソッドが呼ばれず@memo.members
が更新されませんでした。
members_attributesが送られていない場合は@memo.members.clear
を呼ぶことで解決しました。
class MemosController < ApplicationController
def create
Memo.transaction do
@memo = Memo.new(memo_params)
@memo.save!
end
rescue => e
render :new
end
def update
Memo.transaction do
@memo.members.clear if memo_params[:members_attributes].blank?
@memo.update!(memo_params)
end
rescue => e
render :edit
end
private
def memo_params
params.require(:memo).permit(
:title,
:content,
{ members_attributes: [:id] }
)
end
end
これで終わったかに見えるのですが、実は終わってません…。
@memoを保存する前に、memoにmembersのデータが保存されていることを確認しにいってしまい、エラーが発生します。
(@memoはまだ保存されていないので、membersがあるはずもなく、落ちる)
Memoモデルのmembers_attributes=の上書き
これは、members_attributesのデータを元にmemo.members
にデータを反映する過程で発生します。そのため、members_attributes=
メソッドを上書きします。(members_attributes=
メソッドはhas_manyの設定により、ActiveRecordにより、動的に作られるメソッドです。)
class Memo < ApplicationRecord
# (略)
def members_attributes=(attributes)
self.members = User.where(id: attributes.values.pluck('id')
super
end
# (略)
end
これにより、中間テーブルの存在を薄くしてシンプルにMemoとUserのやりとりができるようになりました!
まとめ
polymorphicなデータを扱うときは、リレーションの設定が複雑になるのでハマりやすいですが、設定さえすれば割と綺麗に実装できました。