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

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

PlayerモデルとManagerモデルがあったとして、それぞれにTagモデルをポリモーフィック関連付けをしたい。
ポリモーフィック関連を扱ったアプリでフォームを作成したい場合、何がベストなのか考えている。  

ActiveRecordのおかげでTagオブジェクトに紐づく親オブジェクトは、`@tag.taggable`で簡単に取得できる。  

ただ、PlayerオブジェクトやManagerオブジェクトに紐づくTagオブジェクトを**新規**に作成する場合、
どのように対応するのがベストなのか色々と考えている。  

ちなみに、ポリモーフィック関連でない「１対多」のアソシエーションは以下のような形になる。  

```rb
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する場合で考えてみた。  

- 第１の手段
  - `params[:player_id]`だけでなく、隠しパラメータとして`params[:taggable_type]`を送ってもらう
  - paramsを`Tag.new`に上書きする
  - データベースを経由しないので、悪手である気がする
- 第２の手段
  - 同じく隠しパラメータとして`params[:taggable_type]`も送ってもらう
  - paramsをもってデータベースにアクセスする
  - 紐付け先のテーブルのレコードに基づく形で、`.tags.build`して新しいTagオブジェクトを生成する
  - 中身に関するparamsで上書きする
- 第３の手段
  - requestURLを活用する形でデータベースにアクセスする（RESTfulなURL設計にする必要がある）
  - 紐付け先のテーブルのレコードに基づく形で、`.tags.build`して新しいTagオブジェクトを生成する
  - 中身に関するparamsで上書きする
- 第４の手段
  - ルーティングでネストし、コントローラのディレクトリを切る
  - `player/tags_controller.rb`にて、`params[:player_id]`をもってデータベースにアクセスする
  - 紐付け先のテーブルのレコードに基づく形で、`.tags.build`して新しいTagオブジェクトを生成する
  - 中身に関するparamsで上書きする
- 第５の手段
  - `accepts_nested_attributes_for`を活用する
  - 方法についてはまだ調べていない
  - 複数のモデルに関する属性を更新する場合に使うとよさそう（今回は若干過剰？）
  - フォームオブジェクトも検討すべき？（これこそ過剰な感じがする）

## 第１の手段について

自分なりに考えてみた方法なので、悪手の可能性が高い。
（ちなみに第１と第２の方法を掲載したブログを発見することはできなかった。）  

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`を更新する
  - データベースを経由していないので、存在しないテーブルに紐づくレコードが保存されてしまうかも
  - ただし、モデルの方での制約はあると思われる（未検証）

```rb: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
```

## 第２の手段について

発想としては、ほぼ第１の手段と同じである。  

第１の手段の場合、データベースを経由せず、`Tag.new`に`taggable_id`や`taggable_type`といった
paramsを引数として直接指定しているので、データベースを経由するような形にマイナーチェンジする。  

```rb: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  
```

## 第３の手段について

こちらについては、いくつかのブログで情報が出てきた。
requestURLのパスを活用する方法である。  

  - [猫Rails](http://nekorails.hatenablog.com/entry/2019/06/13/031003)
  - [SkillHub](https://skillhub.jp/courses/145/lessons/1007)

```rb: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  
```

## 第４の手段について

こちらについては、GoRailsという英語のサイトで紹介されている。
猫Railsさんのブログから見つけることができた。  

- [Comments With Polymorphic Associations \(Example\) \| GoRails](https://gorails.com/episodes/comments-with-polymorphic-associations)
- [GitHub: gorails\-screencasts/gorails\-episode\-36](https://github.com/gorails-screencasts/gorails-episode-36)

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

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

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

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

```rb:routes.rb

resources :players do
  resources :tags, module: :players
end
```

```rb: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
```

```rb: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
```

## 第５の手段について

`accepts_nested_attributes_for`を使う方法もある。
この方法については、いくつかのブログがヒットした。  

  - [ポリモーフィックの子から親を生成する \- インターファーム開発部ブログ](http://interfirm.hatenablog.com/entry/2014/07/30/200431)
  - [\[リファクタリングRails\] 親子関係をポリモーフィック関連に変更 \- Qiita](https://qiita.com/kasei-san/items/c6ded7c45ff5ce680e9b)


調べだすと大変なので、詳細についてはここで書かないこととするが、
複数のモデルに関する属性を一度に作成したり更新したい場合、積極的に検討して良いかもしれない。  

また、`accepts_nested_attributes_for`の利用はDHH的にあまり推奨されていない
という話を聞いたことががあるので、フォームオブジェクトを作ってみるのがよいかもしれない。
（今回の場合にそこまでやると、実装がかなり大変になってしまうけど）  

## 注意書き

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