はじめに
- M3Cアドベントカレンダー14日目です。
- 子供にSwitchをねだられています。
対象読者
- 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
を作っておくと、
class StiToy < ApplicationRecord
end
class TvGame < StiToy
def how
'play!'
end
end
class Radicon < StiToy
def how
'move!'
end
end
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)
そう言う時は諦めて Toy
Modelに全て積み込むしかないのでしょうか?
うーん。Toy
Modelの全てのメソッドでこのcase whenが繰り広げられのは辛い。。
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で、
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で管理できるので
管理画面をつくって編集することもできます。
おしまい
ガンダムのプラモデルはまだ早いだろうか。