LoginSignup
1
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-15

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

概要

  • サッカー選手を登録して複数の情報をもたせたい。
  • Playerモデル作成。
  • 持たせたい情報の内、ポジション所属(日本代表とか五輪代表とか)、タイプ(スピードとかパワーとかテクニックとか)は多対多の設計になる。
  • スパイクを登録して複数の情報を持たせたい。
  • Spikeモデル作成。
  • 持たせたい情報の内、適応グラウンドは多対多の設計になる。

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

Image from Gyazo

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

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

Image from Gyazo

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

使い方

Image from Gyazo

Image from Gyazo

赤枠青枠に格納してtype別にTagsテーブルに格納していくイメージを持って下さい。
データ取得はtype別で可能。

赤枠青枠に格納するには、赤枠のモデルを作成してTagモデルを継承させる。

リレーション

playre.rb
class Player < ApplicationRecord
  has_many :player_tags, dependent: :destroy
  has_many :tags, through: :player_tags
end
player_tag.rb
class PlayerTag < ApplicationRecord
  belongs_to :player
  belongs_to :tag
end
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
spike.rb
class Spike < ApplicationRecord
  has_many :spike_tags, dependent: :destroy
  has_many :tags, through: :spike_tags
end
spike_tag.rb
class SpikeTag < ApplicationRecord
  belongs_to :spike
  belongs_to :tag
end

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

Tagモデル継承

position.rb
class PositionTag < Tag
end
belongs.rb
class BelongsTag < Tag
end
genre.rb
class GenreTag < Tag
end
ground.rb
class GroundTag < Tag
end
color.rb
class ColorTag < Tag
end

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

リレーション追加

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

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
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

データを挿入して

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

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と入力してリクエストします。

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 ~~~~~~~~~~ ~~~~~~~~~~

データを取り出してみる

ターミナル
[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はアンチパターンという記事を多く見かけましたが今回の場合は、完全にインターフェースが一緒だったので使ってみました。
スッキリ。

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