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

# はじめに
- [M3Cアドベントカレンダー14日目](https://qiita.com/advent-calendar/2019/m3c)です。
- 子供にSwitchをねだられています。

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

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


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

```sql
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固有のコードをそれぞれのクラスに分けて書くことができ
コードがスッキリしますね！

```ruby
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`を持っているという場合も多いのではないでしょうか。

```sql
-- 親データ
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が繰り広げられのは辛い。。

```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で使うクラスを指定するカラムを作り、

```sql
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で管理できるので
管理画面をつくって編集することもできます。

# おしまい

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