Rails でシングルじゃないテーブル継承

## この記事の目的

Rails の STI を使うようなケースで、STI の代わりに PostgreSQL のテーブル継承を使ってみる方法を紹介します。

## STI とは

- Single Table Inheritance
- 単一テーブル継承
- モデルクラスを継承で表現し、永続化部分はスーパークラスのテーブル１枚でまかなう

## STI の実装例（親クラス）

```create_cars.rb
class CreateCars < ActiveRecord::Migration
  def change
    create_table :cars do |t|
      t.integer :weight
      t.string :color
      t.string :type # STI 用のメタデータカラム
      t.timestamps
    end
  end
end
```

```car.rb
class Car < ActiveRecord::Base; end
```

## STI の実装例（子クラス）

```create_manual_cars.rb
class CreateManualCars < ActiveRecord::Migration
  def change
    # スーパークラスのテーブルにカラムを追加するだけ
    add_column :cars, :number_of_gears, :integer
  end
end
```

```manual_car.rb
class ManualCar < Car
end
```

```automatic_car.rb
class AutomaticCar < Car
end
```

## STI の便利さ

- サブクラス特有のカラムを add_column して、スーパークラスを継承するだけで作れちゃう便利
- スーパークラスに共通処理や属性を持たせることで、サブクラスのコードがスッキリ！
- メタデータカラムがあるので、親クラスのインスタンスから、子クラスを特定出来たりする

```rb
AutomaticCar.create(
  weight: 1000,
  color:  "blue"
) #=> #<AutomaticCar id: 1>

# ダウンキャストっぽい事ができる
Car.find(1).tap{|car|
  break car.type.classify.constantize.find(car.id)
}.class #=> AutomaticCar
```
  
## STI の注意点

- スーパークラスからサブクラスの属性に触れちゃう

```rb
Car.find(1).number_of_gears #=> 6
Car.find(1).update(number_of_gears: 5)
```

- それを防ぐためには、守るコードを追加する必要がある

```rb
# gem 'protected_attributes'
class Car < ActiveRecord::Base
  attr_accessible :weight, :color
end

class ManualCar < Car
  attr_accessible :number_of_gears
end
```

## 外部キー制約

- ついに Rails 本体に外部キー制約サポートがくるよー
  
> Support for real foreign keys!
>  add_foreign_key/remove_foreign_key are now available in migrations.
> http://weblog.rubyonrails.org/2014/8/20/Rails-4-2-beta1/

## STI と外部キー制約

子クラス特有の属性（a_id とします）を定義するようなケースで、a_id が外部キーであり、a_id に NOT NULL 制約と外部キー制約を付けたいとします。
素の STI では、親や他の子クラスから INSERT したら a_id に NULL が入るようになってますので、NOT NULL 制約を付けるとエラーになります。
じゃあ、外部キーが指してる外部テーブル側に {id: 99999, value: "無し"} みたいなレコードを入れておいて、外部キーに NULL が入りそうになったら、代わりに 99999 を入れることで解決するかというと、動くかもしれないけど辛いですね。

## STI の背景

そもそも STI はどこから来たのか

> Relational databases don't support inheritance, so when mapping...
> http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html

でも、テーブルの継承って機能が PostgreSQL にありますよね。STI がやりたいことって、テーブル継承でも実現できそう！


## 継承テーブルの作成

```rb
class CreateCars < ActiveRecord::Migration
  def change
    create_table :cars do |t|
      t.integer :weight
      t.string :color
      # t.string :type STI じゃないので、これは不要
      t.timestamps
    end
  end
end

class AddManualCar < ActiveRecord::Migration
  def up
    execute <<-SQL
      CREATE TABLE manual_cars(number_of_gears integer)
          INHERITS cars
    SQL
  end

  def down
    drop_table :manual_cars
  end
end

class AddAutomaticCar < ActiveRecord::Migration
  def up
    execute "CREATE TABLE automatic_cars() INHERITS cars"
  end

  def down
    drop_table :automatic_cars
  end
end
```

`INHERITS cars` という部分が、cars テーブルの属性を継承する宣言となります。

## STI のレールから降りるの儀

```rb
# テーブル継承の親になるモデルの振る舞いを表すモジュール
module Parental
  # 親クラスのインスタンスから子クラスのインスタンスに変身する
  def down_cast
    sub_model.find(self)
  end
 
  private
  # システム行 - tableoid
  # この列は特に、継承階層からの選択問い合わせでは便利です。
  # tableoidはテーブル名を得るためにpg_classのoid列に結合することができます。
  # -- PostgreSQL のマニュアルより
  def sub_model
    relation_name = ActiveRecord::Base.connection.execute(<<-SQL).first["relname"]
      SELECT relname
        FROM #{self.class.to_s.tableize} p, pg_class c
       WHERE p.tableoid = c.oid
         AND p.id = #{self.id}
       LIMIT 1
    SQL

    # クラス名と対応したテーブル名をつける規約に従っている前提で
    relation_name.classify.constantize
  end
end
```

```rb
# テーブル継承の子になるモデルの振る舞いを表すモジュール
module PgInherits
  extend ActiveSupport::Concern
 
  included do |base|
    # 普通にクラス継承をすると親クラスのテーブル名なので、上書きする
    base.table_name = base.name.tableize
  end
end
```

```rb
# モデルクラスの実装
class Car < ActiveRecord::Base
  include Parental
end

class ManualCar < Car
  include PgInherits
end

class AutomaticCar < Car
  include PgInherits
end
```

## テーブル継承の注意点

- 多重継承（１つのテーブルに複数の親テーブルを設定）ができるが、多重継承の無いプログラミング言語とマッピングしづらいです
- 親テーブルで PRIMARY KEY, UNIQUE の宣言をしていても、子テーブルまでは制約が伝播しません
- 親テーブルに外部キーを持って、外部キー制約をつけていても、子テーブルまで制約が伝播しません
- NOT NULL や DEFAULT は子テーブルに伝播します
- PRIMARY KEY, UNIQUE について、親まで遡らないので、子テーブルから重複値を入れ放題
 - 子テーブルに別途制約を付けても、子テーブル内でしかチェックしないので根本解決になりません
 - Rails で困る具体例は id

```rb
# 親クラスのレコードを作成
Car.create #=> #<Car id: 1>

# 子クラスで id を明示してレコードを作成
AutomaticCar.create(id: 1) #=> #<AutomaticCar id: 1>

# id: 1 が被ってる
Car.where(id: 1).count #=> 2
```

ただ、DEFAULT は伝播します。
ID シーケンスは親テーブルと共通なので、ID を直接指定して INSERT や UPDATE をしたりしなければ問題ないです。しなければ。するな。でも fixtures とかががが
回避策としては、下記のように id 属性の使用に制限をかける方法があります。

```rb
module PgInherits
  extend ActiveSupport::Concern
 
  included do |base|
    base.table_name = base.name.tableize
    # gem 'protected_attributes' こいつ便利...
    attr_protected :id
  end
end
```

## まとめ

STI は便利ですが、DB のメンテがしづらくなるので、なるべく避けたいです。今回の方法は STI の代わりになりえると思います。
なお、PostgreSQL のテーブル継承は、今日の他エントリ（[PostgreSQL のパーティションテーブル自動生成](http://qiita.com/yancya/items/960180ed1d6faa510d65)）でも使っていますので、そちらもご覧いただければと思います。

## 12 月 18 日
今日は誕生日なので４本の Advent Calendar を書きました。よろしければ、このエントリの他に下記のエントリもご覧ください。

- [Refinements のスコープについて](http://qiita.com/yancya/items/b5c8fa2cd53eaa650e65)
- [PostgreSQL のパーティションテーブル自動生成](http://qiita.com/yancya/items/960180ed1d6faa510d65)
- [誕生日だ](http://upec.jp/archives/592)

誕生日ですが「[ウィッシュリスト](http://www.amazon.co.jp/registry/wishlist/34SGMMW6WQGZM)に入れてるからには読めよ」などと言いながら難しい本を送りつけるなどの行為は何卒