1
2

More than 3 years have passed since last update.

【自分用メモ】ポリモーフィックな関連付けをして、フォームを作成する場合の方法論

Last updated at Posted at 2020-08-30

ポリモーフィックな関連付けをした場合のフォーム

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: 2manager_id:3というparamsが送られてくる場合、
strong_paramsをどうするのか問題を解決する必要がある。

そこで、フォームから送られてくるparamsの形式を揃えてしまえばよい。

  • 隠れパラメータとしてtaggable_idtaggable_typeをフォームから送る
    • <%= form.hidden_field :taggable_id, value: player.id %>
    • <%= form.hidden_field :taggable_type, value: player.class %>
    • もちろん、Managerオブジェクトであれば、valueはmanager.idmanager.classになる
  • 隠れパラメータを活用して、Tag.newを更新する
    • データベースを経由していないので、存在しないテーブルに紐づくレコードが保存されてしまうかも
    • ただし、モデルの方での制約はあると思われる(未検証)
tags_controller.rb

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.newtaggable_idtaggable_typeといった
paramsを引数として直接指定しているので、データベースを経由するような形にマイナーチェンジする。

tags_controller.rb

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のパスを活用する方法である。

tags_controller.rb

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さんのブログから見つけることができた。

ルーティングでplayerやmanagerのresourcesの下に、tagsのresourcesをネストさせてしまい、
コントローラのディレクトリを分ける方法である。

  1. tags_controller.rbという親玉コントローラ
  2. 親玉コントローラを継承するplayer/tags_controller.rbという子分コントローラ
  3. その親玉コントローラを継承するmanagers/tags_controller.rbという子分コントローラ

以上3つのコントローラを作ってしまうのであまりDRYではない気もするが、
モデルによって異なるロジックを書きたい場合、こちらの方法を採用する方がよいだろう。

Railsの規約に則っているような気がするので、この方法がベストなのかなという気がする。
(印象論で適当に言っているだけですが)

routes.rb

resources :players do
  resources :tags, module: :players
end
tags_controller.rb

def create
  @tag = @taggable.tags.build(tag_params)

  # 他は省略するが`@tag`をsave + redirectする(失敗した場合、render)
end

private

  def tag_params
    params.require(:tag).permit(:body)
  end
players/tags_controller.rb
# 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的にあまり推奨されていない
という話を聞いたことががあるので、フォームオブジェクトを作ってみるのがよいかもしれない。
(今回の場合にそこまでやると、実装がかなり大変になってしまうけど)

注意書き

初学者が書いています。
しかも読みやすさという点でも完成度が低いので、保険をかけてます。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2