ポリモーフィックな関連付けをした場合のフォーム
PlayerモデルとManagerモデルがあったとして、それぞれにTagモデルをポリモーフィック関連付けをしたい。
ポリモーフィック関連を扱ったアプリでフォームを作成したい場合、何がベストなのか考えている。
ActiveRecordのおかげでTagオブジェクトに紐づく親オブジェクトは、@tag.taggable
で簡単に取得できる。
ただ、PlayerオブジェクトやManagerオブジェクトに紐づくTagオブジェクトを新規に作成する場合、
どのように対応するのがベストなのか色々と考えている。
ちなみに、ポリモーフィック関連でない「1対多」のアソシエーションは以下のような形になる。
def tag_params
params.require(:tag).permit(:body).merge(player_id: params[:player_id])
end
ポリモーフィック関連の特有の問題
ここで問題なのが、merge以下の箇所。
-
params[:player_id]
の場合もあれば、params[:manager_id]
の場合もある - ポリモーフィックなので、
taggable_type(紐付け先のテーブル名)
についての情報も取得する必要がある
そこで、いくつか方法論を調べたり、考えたりしてみた。
例えば、Playerモデルに紐づくTagオブジェクトをcreateする場合で考えてみた。
- 第1の手段
-
params[:player_id]
だけでなく、隠しパラメータとしてparams[:taggable_type]
を送ってもらう - paramsを
Tag.new
に上書きする - データベースを経由しないので、悪手である気がする
-
- 第2の手段
- 同じく隠しパラメータとして
params[:taggable_type]
も送ってもらう - paramsをもってデータベースにアクセスする
- 紐付け先のテーブルのレコードに基づく形で、
.tags.build
して新しいTagオブジェクトを生成する - 中身に関するparamsで上書きする
- 同じく隠しパラメータとして
- 第3の手段
- requestURLを活用する形でデータベースにアクセスする(RESTfulなURL設計にする必要がある)
- 紐付け先のテーブルのレコードに基づく形で、
.tags.build
して新しいTagオブジェクトを生成する - 中身に関するparamsで上書きする
- 第4の手段
- ルーティングでネストし、コントローラのディレクトリを切る
-
player/tags_controller.rb
にて、params[:player_id]
をもってデータベースにアクセスする - 紐付け先のテーブルのレコードに基づく形で、
.tags.build
して新しいTagオブジェクトを生成する - 中身に関するparamsで上書きする
- 第5の手段
-
accepts_nested_attributes_for
を活用する - 方法についてはまだ調べていない
- 複数のモデルに関する属性を更新する場合に使うとよさそう(今回は若干過剰?)
- フォームオブジェクトも検討すべき?(これこそ過剰な感じがする)
-
第1の手段について
自分なりに考えてみた方法なので、悪手の可能性が高い。
(ちなみに第1と第2の方法を掲載したブログを発見することはできなかった。)
Tagオブジェクトが紐づく先のモデルが異なると、
フォームから送信されるparamsのハッシュのキーが異なってしまう。
つまり、player_id: 2
やmanager_id:3
というparamsが送られてくる場合、
strong_paramsをどうするのか問題を解決する必要がある。
そこで、フォームから送られてくるparamsの形式を揃えてしまえばよい。
- 隠れパラメータとして
taggable_id
とtaggable_type
をフォームから送る<%= form.hidden_field :taggable_id, value: player.id %>
<%= form.hidden_field :taggable_type, value: player.class %>
- もちろん、Managerオブジェクトであれば、valueは
manager.id
やmanager.class
になる
- 隠れパラメータを活用して、
Tag.new
を更新する- データベースを経由していないので、存在しないテーブルに紐づくレコードが保存されてしまうかも
- ただし、モデルの方での制約はあると思われる(未検証)
def create
@tag = Tag.new(tag_params)
# 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end
private
def tag_params
params.require(:tag).permit(:body, :taggable_id, :taggable_type)
end
第2の手段について
発想としては、ほぼ第1の手段と同じである。
第1の手段の場合、データベースを経由せず、Tag.new
にtaggable_id
やtaggable_type
といった
paramsを引数として直接指定しているので、データベースを経由するような形にマイナーチェンジする。
def create
@tag = @taggable.tags.build(tag_params)
# 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end
private
def set_taggable
# constantizeメソッドを使うことで文字を定数化できる
@taggable = tag_params[:taggable_type].constantize.find(tag_params[:taggable_id])
end
def tag_params
params.require(:tag).permit(:body)
end
第3の手段について
こちらについては、いくつかのブログで情報が出てきた。
requestURLのパスを活用する方法である。
def create
@tag = @taggable.tags.build(tag_params)
# 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end
private
def set_taggable
# `/players/2/tags`の場合、'players'と'2'という要素を取得できる
resource, id = request.path.split('/')[1,2]
# `/player/2/tag`の場合、Player.find(2)となる
# singularizeメソッドにより、'players'が'player'になる
# classifyメソッドにより、'player'が'Player'になる
# constantizeメソッドを使うことで文字を定数化できる
@taggable = resource.singularize.classify.constantize.find(id)
end
def tag_params
params.require(:tag).permit(:body)
end
第4の手段について
こちらについては、GoRailsという英語のサイトで紹介されている。
猫Railsさんのブログから見つけることができた。
- Comments With Polymorphic Associations (Example) | GoRails
- GitHub: gorails-screencasts/gorails-episode-36
ルーティングでplayerやmanagerのresourcesの下に、tagsのresourcesをネストさせてしまい、
コントローラのディレクトリを分ける方法である。
-
tags_controller.rb
という親玉コントローラ - 親玉コントローラを継承する
player/tags_controller.rb
という子分コントローラ - その親玉コントローラを継承する
managers/tags_controller.rb
という子分コントローラ
以上3つのコントローラを作ってしまうのであまりDRYではない気もするが、
モデルによって異なるロジックを書きたい場合、こちらの方法を採用する方がよいだろう。
Railsの規約に則っているような気がするので、この方法がベストなのかなという気がする。
(印象論で適当に言っているだけですが)
resources :players do
resources :tags, module: :players
end
def create
@tag = @taggable.tags.build(tag_params)
# 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end
private
def tag_params
params.require(:tag).permit(:body)
end
# tags_controller.rb を継承する
class Players::TagsController < TagsController
before_action :set_taggable
private
def set_taggable
@taggable = Player.find(params[:player_id])
end
end
第5の手段について
accepts_nested_attributes_for
を使う方法もある。
この方法については、いくつかのブログがヒットした。
調べだすと大変なので、詳細についてはここで書かないこととするが、
複数のモデルに関する属性を一度に作成したり更新したい場合、積極的に検討して良いかもしれない。
また、accepts_nested_attributes_for
の利用はDHH的にあまり推奨されていない
という話を聞いたことががあるので、フォームオブジェクトを作ってみるのがよいかもしれない。
(今回の場合にそこまでやると、実装がかなり大変になってしまうけど)
注意書き
初学者が書いています。
しかも読みやすさという点でも完成度が低いので、保険をかけてます。