[Rails]STI使ったらDBがスッキリした

STI使ったらテーブルがスッキリした話し。

# 概要

- サッカー選手を登録して複数の情報をもたせたい。

> - `Player`モデル作成。
- 持たせたい情報の内、`ポジション`、`所属`(日本代表とか五輪代表とか)、`タイプ`(スピードとかパワーとかテクニックとか)は多対多の設計になる。

- スパイクを登録して複数の情報を持たせたい。

> - `Spike`モデル作成。
> - 持たせたい情報の内、`適応グラウンド`、`色`は多対多の設計になる。

↓最初はこんな設計になりました。

[![Image from Gyazo](https://i.gyazo.com/1f82d815d09a35c4bfcb555bff410127.png)](https://gyazo.com/1f82d815d09a35c4bfcb555bff410127)

見ての通り冗長だし、他に多対多で持たせたい情報が後から追加されるたびにテーブル2つ追加しないといけない。

ということでSTIを使うとこうなる。

[![Image from Gyazo](https://t.gyazo.com/teams/startup-technology/cc56422f7d7847530f182c4cbd6ef1d7.png)](https://startup-technology.gyazo.com/cc56422f7d7847530f182c4cbd6ef1d7)

めちゃくちゃスッキリして、多対多で持たせたい情報を増やしたい場合もテーブルを新たに作成する必要はありません。

# 使い方

[![Image from Gyazo](https://i.gyazo.com/97eec3be9b2668cf2b6e9e05d69c4701.png)](https://gyazo.com/97eec3be9b2668cf2b6e9e05d69c4701)

[![Image from Gyazo](https://i.gyazo.com/d82b80abea9b2cadcf4bb92573beb7d3.png)](https://gyazo.com/d82b80abea9b2cadcf4bb92573beb7d3)

<font color="Red">赤枠</font>を<font color="RoyalBlue">青枠</font>に格納してtype別にTagsテーブルに格納していくイメージを持って下さい。
データ取得はtype別で可能。

<font color="Red">赤枠</font>を<font color="RoyalBlue">青枠</font>に格納するには、<font color="Red">赤枠</font>のモデルを作成してTagモデルを継承させる。

## リレーション

```ruby:playre.rb
class Player < ApplicationRecord
  has_many :player_tags, dependent: :destroy
  has_many :tags, through: :player_tags
end
```

```ruby:player_tag.rb
class PlayerTag < ApplicationRecord
  belongs_to :player
  belongs_to :tag
end
```

```ruby:tag.rb
class Tag < ApplicationRecord
  has_many :player_tags, dependent: :destroy
  has_many :players, through: :player_tags
  has_many :spike_tags, dependent: :destroy
  has_many :spikes, through: :spike_tags
end
```

```ruby:spike.rb
class Spike < ApplicationRecord
  has_many :spike_tags, dependent: :destroy
  has_many :tags, through: :spike_tags
end
```

```ruby:spike_tag.rb
class SpikeTag < ApplicationRecord
  belongs_to :spike
  belongs_to :tag
end

```

ここまでは普通に多対多のリレーションシップです。
STIを使用するには<font color="Red">赤枠</font>のモデルを作成して**Tagモデルを継承**させる必要があります。

## Tagモデル継承

```ruby:position.rb
class PositionTag < Tag
end
```

```ruby:belongs.rb
class BelongsTag < Tag
end
```

```ruby:genre.rb
class GenreTag < Tag
end
```

```ruby:ground.rb
class GroundTag < Tag
end
```

```ruby:color.rb
class ColorTag < Tag
end
```

今後多対多で増やしたい情報があったらこんな感じでtypeカラムに突っ込みたいモデルを作成してTagモデルを継承させてあげればOK。

## リレーション追加

`player.position_tags`とか`spike.ground_tags`とかでデータを取り出せるようにリレーションを追加していきます。

```ruby:playre.rb
class Player < ApplicationRecord
  has_many :player_tags, dependent: :destroy
  has_many :tags, through: :player_tags
  # 追加
  has_many :position_tags, through: :player_tags
  has_many :genre_tags, through: :player_tags
  has_many :belongs_tags, through: :player_tags
end
```

```ruby:player_tag.rb
class PlayerTag < ApplicationRecord
  belongs_to :player
  belongs_to :tag
  # 追加
  has_many :ground_tags, through: :spike_tags
  has_many :color_tags, through: :spike_tags
end
```

ここが「はっ？わけわからん？」ポイントでした。
`position_tags`とか`ground_tags`とかのテーブルどこにあんの？
結論言うと、Tagsテーブルのtypeカラムの中身と各々リレーションシップが組まれています。
RailsにおけるtypeカラムはSTI専用の特別なカラムになるので普段は使わないように注意が必要です。

[![Image from Gyazo](https://i.gyazo.com/9fe36201559d58955ee530eb5b893256.png)](https://gyazo.com/9fe36201559d58955ee530eb5b893256)

## データを挿入して

実際にはこのような使い方はしませんが、流れ追いたいのでtagへのデータ挿入部分だけ記述します。

```slim:views/players/new.html.slim
= form_with model: @player, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: "form-control"
  .form-group
    = f.label :position
    = f.text_field :position, class: "form-control"
  .action.mt-3
    = f.submit class: "btn btn-primary"
```

上記で送信すると`params[:player][:position]`に格納されてサーバーへリクエストを投げます。
複数の文字列を送信したい場合は任意の文字列(`/`とか`,`とか)で区切ってください。
今回は`FW/MF`と入力してリクエストします。

```ruby:players.controller.rb
def create
  player = Player.new(player)
  position_tag_names = params[:player][:position]
  player.save
  # ポイント
  player.position_tags = position_tag_names.split('区切りたい文字列').map { |name| PositionTag.find_or_create_by(name: name) }
end

・
・

private

def player_params
  params.require(:player).permit(:name)
end
```

`ポイント`以下1文が肝です。
本来はモデルで処理するのが望ましいと思います。

`position_tag_names`へ任意の区切りたい文字列(`/`とか`,`とか)毎にstringを作成して(split)、それを一つずつ配列へ入れて(map)登録する、もしくは先に登録がされていたらそれを返す。(find_or_create_by)

データベースを見てみると下記のように登録がされています。

| id  |type         |name  |created_at|updated_at|
|:----|:------------|:-----|:---------|:---------|
| 1   |PositionTag  |FW    |~~~~~~~~~~|~~~~~~~~~~|
| 2   |PositionTag  |MF    |~~~~~~~~~~|~~~~~~~~~~|

## データを取り出してみる

```console:ターミナル
[1] pry(main)> player = Player.first
   (1.4ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
  Player Load (0.5ms)  SELECT  `players`.* FROM `players` ORDER BY `players`.`id` ASC LIMIT 1
=> #<Player:0x00007fcea7e69718 id: 1, name: "SS2ゴハン", created_at: Mon, 15 Jun 2020 18:13:45 JST +09:00, updated_at: Mon, 15 Jun 2020 18:13:45 JST +09:00, spike_id: 1>
[2] pry(main)> player.position_tags
  PositionTag Load (1.6ms)  SELECT `tags`.* FROM `tags` INNER JOIN `player_tags` ON `tags`.`id` = `player_tags`.`tag_id` WHERE `tags`.`type` IN ('PositionTag') AND `player_tags`.`player_id` = 1
=> [#<PositionTag:0x00007fcea8f3a498 id: 1, type: "PositionTag", name: "FW", created_at: Mon, 15 Jun 2020 18:13:45 JST +09:00, updated_at: Mon, 15 Jun 2020 18:13:45 JST +09:00>,
 #<PositionTag:0x00007fcea8f3a2e0 id: 2, type: "PositionTag", name: "MF", created_at: Mon, 15 Jun 2020 18:13:45 JST +09:00, updated_at: Mon, 15 Jun 2020 18:13:45 JST +09:00>]
```

`player.position_tags`でそのプレイヤーに紐づく`position_tags`がとってこれます。
あとはeachで回して一つずつ表示させたり好きなようにできます。

## 最後に

STIはアンチパターンという記事を多く見かけましたが今回の場合は、完全にインターフェースが一緒だったので使ってみました。
スッキリ。