STI使ったらテーブルがスッキリした話し。
概要
- サッカー選手を登録して複数の情報をもたせたい。
Player
モデル作成。
-
持たせたい情報の内、
ポジション
、所属
(日本代表とか五輪代表とか)、タイプ
(スピードとかパワーとかテクニックとか)は多対多の設計になる。 -
スパイクを登録して複数の情報を持たせたい。
Spike
モデル作成。- 持たせたい情報の内、
適応グラウンド
、色
は多対多の設計になる。
↓最初はこんな設計になりました。
見ての通り冗長だし、他に多対多で持たせたい情報が後から追加されるたびにテーブル2つ追加しないといけない。
ということでSTIを使うとこうなる。
めちゃくちゃスッキリして、多対多で持たせたい情報を増やしたい場合もテーブルを新たに作成する必要はありません。
使い方
赤枠を青枠に格納してtype別にTagsテーブルに格納していくイメージを持って下さい。
データ取得はtype別で可能。
赤枠を青枠に格納するには、赤枠のモデルを作成してTagモデルを継承させる。
リレーション
class Player < ApplicationRecord
has_many :player_tags, dependent: :destroy
has_many :tags, through: :player_tags
end
class PlayerTag < ApplicationRecord
belongs_to :player
belongs_to :tag
end
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
class Spike < ApplicationRecord
has_many :spike_tags, dependent: :destroy
has_many :tags, through: :spike_tags
end
class SpikeTag < ApplicationRecord
belongs_to :spike
belongs_to :tag
end
ここまでは普通に多対多のリレーションシップです。
STIを使用するには赤枠のモデルを作成してTagモデルを継承させる必要があります。
Tagモデル継承
class PositionTag < Tag
end
class BelongsTag < Tag
end
class GenreTag < Tag
end
class GroundTag < Tag
end
class ColorTag < Tag
end
今後多対多で増やしたい情報があったらこんな感じでtypeカラムに突っ込みたいモデルを作成してTagモデルを継承させてあげればOK。
リレーション追加
player.position_tags
とかspike.ground_tags
とかでデータを取り出せるようにリレーションを追加していきます。
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
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専用の特別なカラムになるので普段は使わないように注意が必要です。
データを挿入して
実際にはこのような使い方はしませんが、流れ追いたいのでtagへのデータ挿入部分だけ記述します。
= 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
と入力してリクエストします。
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 | ~~~~~~~~~~ | ~~~~~~~~~~ |
データを取り出してみる
[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はアンチパターンという記事を多く見かけましたが今回の場合は、完全にインターフェースが一緒だったので使ってみました。
スッキリ。