1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

エムスリーキャリアAdvent Calendar 2019

Day 14

[RubyOnRails] 親データでSTIするクリスマス

Last updated at Posted at 2019-12-13

はじめに

対象読者

  • STI使いたいけど、親-子関係のレガシーなテーブルでどうしようと思っている人

STIとは

typeというカラムがあるテーブルをRailsで扱おうとすると通常とは違った動きになりますよね。
Railsがそのレコードをインスタンス化するときにtypeカラムにある名前のクラスのインスタンスに
自動でしてくれる(しまう)からです。

こんなテーブルがあったときに、

m3c_christmas=# select id, name, type from sti_toys;
 id |     name     |     type
----+--------------+--------------
  1 | マリオ       | TvGame
  2 | ポケモン     | TvGame
  3 | ゼビウス     | TvGame
  4 | スポーツカー | Radicon
  5 | 戦車         | Radicon
  6 | ジープ       | Radicon
  7 | ガンダム     | PlasticModel
  8 | ダンバイン   | PlasticModel
  9 | ボトムズ     | PlasticModel
(9 rows)

こんなModelを作っておくと、

app/models/sti_toy.rb
class StiToy < ApplicationRecord
end
app/models/tv_game.rb
class TvGame < StiToy
  def how
    'play!'
  end
end
app/models/radicon.rb
class Radicon < StiToy
  def how
    'move!'
  end
end
app/models/plastic_model.rb
class PlasticModel < StiToy
  def how
    'make!'
  end
end

データを取得するときに、
自動的にクラスが別れるので、そのtype固有のコードをそれぞれのクラスに分けて書くことができ
コードがスッキリしますね!

irb(main):008:0> toy = StiToy.find(1)
  StiToy Load (0.9ms)  SELECT  "sti_toys".* FROM "sti_toys" WHERE "sti_toys"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
=> #<TvGame id: 1, name: "マリオ", type: "TvGame", price: nil, comment: nil, created_at: "2019-12-13 10:44:51", updated_at: "2019-12-13 10:44:51">
irb(main):009:0> toy.how
=> "play!"
irb(main):010:0> toy = StiToy.find(4)
  StiToy Load (0.8ms)  SELECT  "sti_toys".* FROM "sti_toys" WHERE "sti_toys"."id" = $1 LIMIT $2  [["id", 4], ["LIMIT", 1]]
=> #<Radicon id: 4, name: "スポーツカー", type: "Radicon", price: nil, comment: nil, created_at: "2019-12-13 10:47:25", updated_at: "2019-12-13 10:47:25">
irb(main):011:0> toy.how
=> "move!"
irb(main):012:0> toy = StiToy.find(7)
  StiToy Load (0.5ms)  SELECT  "sti_toys".* FROM "sti_toys" WHERE "sti_toys"."id" = $1 LIMIT $2  [["id", 7], ["LIMIT", 1]]
=> #<PlasticModel id: 7, name: "ガンダム", type: "PlasticModel", price: nil, comment: nil, created_at: "2019-12-13 10:48:30", updated_at: "2019-12-13 10:48:30">
irb(main):013:0> toy.how
=> "make!"

が、しかし

新しいサービスを作る際はSTIを意識して作ればよいですが、すでにあるサービスの場合、
typeに相当する親データのテーブルがあり、そのidを持っているという場合も多いのではないでしょうか。

-- 親データ
m3c_christmas=# select id, name from toy_types;
 id |     name
----+--------------
  1 | テレビゲーム
  2 | ラジコン
  3 | プラモデル
(3 rows)

m3c_christmas=# select name, toy_type_id from toys;
     name     | toy_type_id
--------------+-------------
 マリオ       |           1
 ポケモン     |           1
 ゼビウス     |           1
 スポーツカー |           2
 戦車         |           2
 ジープ       |           2
 ガンダム     |           3
 ダンバイン   |           3
 ボトムズ     |           3
(9 rows)

そう言う時は諦めて ToyModelに全て積み込むしかないのでしょうか?
うーん。ToyModelの全てのメソッドでこのcase whenが繰り広げられのは辛い。。

app/models/toy.rb
class Toy < ApplicationRecord
  def how
    case toy_type_id
    when 1
      'play!'
    when 2
      'move!'
    when 3
      'make!'
    end
  end
end

かと言ってSTIするためにtoy_type_idと被るtypeカラムを作るのも冗長だし。。。

親データでSTI

親データのテーブルに、STIで使うクラスを指定するカラムを作り、

m3c_christmas=# select id, name, sti_type from toy_types;
 id |     name     |   sti_type
----+--------------+--------------
  1 | テレビゲーム | TvGame
  2 | ラジコン     | Radicon
  3 | プラモデル   | PlasticModel
(3 rows)

子データのModelで、

app/models/toy.rb
class Toy < ApplicationRecord
  self.inheritance_column = :toy_type_id

  def self.find_sti_class(type)
    @@types ||= ToyType.all.map{|type| [type.id, type.sti_type] }.to_h
    klass = @@types[type]
    return const_get(klass) if klass.present?

    # toy_typesが追加になっているかもしれないので取り直す。
    @@types = ToyType.all.map{ |type| [type.id, type.sti_type] }.to_h
    const_get(@@types[type])
  end
end

とやれば、どの親データに紐づいているかでSTIのクラスを分けることができます。
普通のSTIだと各データにModel名が入っているので、万が一Model名を変更することになったら
ドキドキなデータメンテがいりますが、これなら親データ側の変更だけなので少なくて済みます。

STIしたクラスに持たせたい属性もコードでなく、DBで管理できるので
管理画面をつくって編集することもできます。

おしまい

ガンダムのプラモデルはまだ早いだろうか。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?