Railsで使うデータベース上の特別な意味を持つ列名 その参 type

前回、前々回に続いて、RailsのMagic Field Namesです。
今回は"type"というフィールドについて。
RailsWikiによると

Single table inheritance

Active Record allows inheritance by storing the name of the class in a column that by default is called “type” (can be changed by overwriting Base.inheritance_column).

See the Single table inheritance section of the Active Record API docs.

だそうです。
"Single table inheritance"を実現するのに"type"を使えばいいよ。
しかし、まず"Single table inheritance"を知らなければ、なりません。

Single Table Inheritance

"Single Table Inheritance"とは、プログラム上のクラスの継承関係をリレーショナルデータベース上でどのように表すかを決めた手法の一つです。
Martin Fowlerという人が、3つのパターンを提唱しているそうです。

  1. Class Table Inheritance
  2. Single Table Inheritance
  3. Concrete Table Inheritance

上のリンクの絵を見てもらえば、大体は分かると思います。
"Single table inheritance"では、子クラスも親クラスも一つのテーブルに詰め込んでしまえ。けど、誰が誰だか判らなくなるから、typeって列を使ってクラス名を入れておこう。ってな感じです。

検証

Single Table Inheritanceっぽく、Player、Footballer、Cricketer、Bowlerを作ってみる。
まずは db/migreate/001_create_players.rb

class CreatePlayers < ActiveRecord::Migration
  def self.up
    create_table :players do |t|
      t.column :name, :string
      t.column :club, :string
      t.column :batting_average, :integer
      t.column :bowling_average, :integer
      t.column :type, :string
    end
  end

  def self.down
    drop_table :players
  end
end

MySQLではこんな感じ。

mysql> desc players;
+-----------------+--------------+------+-----+---------+----------------+
| Field           | Type         | Null | Key | Default | Extra          |
+-----------------+--------------+------+-----+---------+----------------+
| id              | int(11)      | NO   | PRI | NULL    | auto_increment |
| name            | varchar(255) | YES  |     | NULL    |                |
| club            | varchar(255) | YES  |     | NULL    |                |
| batting_average | int(11)      | YES  |     | NULL    |                |
| bowling_average | int(11)      | YES  |     | NULL    |                |
| type            | varchar(255) | YES  |     | NULL    |                |
+-----------------+--------------+------+-----+---------+----------------+

でapp/models以下。それぞれ必須にしちゃう。
player.rb

class Player < ActiveRecord::Base
  validates_presence_of :name
end

footballer.rb

class Footballer < Player
  validates_presence_of :club
end

cricketer.rb

class Cricketer < Player
  validates_presence_of :batting_average
end

bowler.rb

class Bowler < Cricketer
  validates_presence_of :bowling_average
end

Playerモデルにちょいと便利なメソッドを追加。app/models/player.rb

(略)
def self.factory(type, params = nil)
  case type
    when 'Footballer'
      return Footballer.new(params)
    when 'Cricketer'
      return Crickter.new(params)
    when 'Bowler'
      return Bowler.new(params)
    else
      return nil
  end
end
(略)

Scaffold作成後、プレイヤーの新規作成をちょいと変更。
app/views/players/list.rhtml

(略 テーブル表示部分)
<%= link_to 'New footballer', :action => 'new', :player_type => 'Footballer' %>
<%= link_to 'New Cricketer', :action => 'new', :player_type => 'Cricketer' %>
<%= link_to 'New Bowler', :action => 'new', :palyer_type => 'Bowler' %>

app/controllers/players_controller.rbのnewメソッドも変更。

(略)
def new
  @player = Player.factory(params[:player_type])
end

app/views/players/_form.rbも変更。
is_a?メソッドを活用します。

<%= error_messages_for 'player' %>

<!--[form:player]-->
<p><label for="player_name">Name</label><br/>
<%= text_field 'player', 'name'  %></p>

<% if @player.is_a?(Footballer) %>
  <p><label for="player_club">Club</label><br/>
  <%= text_field 'player', 'club'  %></p>
<% end %>

<% if @player.is_a?(Cricketer) %>
  <p><label for="player_batting_average">Batting average</label><br/>
  <%= text_field 'player', 'batting_average'  %></p>

  <% if @player.is_a?(Bowler) %>
    <p><label for="player_bowling_average">Bowling average</label><br/>
    <%= text_field 'player', 'bowling_average'  %></p>
  <% end %>
<% end %>
<!--[eoform:player]-->
<%= hidden_field_tag 'player_type', @player[:type] %>

app/controllers/players_controller.rbのcreateメソッドも変更。

def create
  @player = Player.factory(params[:player_type], params[:player])
  if @player.save
    flash[:notice] = 'Player was successfully created.'
    redirect_to :action => 'list'
  else
    render :action => 'new'
  end
end

適当に値を入力してみる。

データベース上はこんな感じ。

+----+--------------+--------------+-----------------+-----------------+------------+
| id | name         | club         | batting_average | bowling_average | type       |
+----+--------------+--------------+-----------------+-----------------+------------+
|  1 | Footballer 1 | ユナイテッド |            NULL |            NULL | Footba     |
|  2 | Cricketer 1  | NULL         |             100 |            NULL | Cricketer  |
|  3 | Bowler 1     | NULL         |              99 |             102 | Bowler     |
+----+--------------+--------------+-----------------+-----------------+------------+


app/controllers/players_controller.rbのeditやupdateは変更しなくておk。
既存のものをDBからfindしてくる場合は、Railsがtype列を見て、適当なクラスのインスタンスを返してくれます。
上の場合、Player.find(2)を行えばPlayerのインスタンスではなくて、Footballerのインスタンスを返してくれます。

要点

Single Table Inheritanceを使うときは"type"列を使うとやりやすくなるよ。
Single Table Inheritanceを知らないと、意味無いよ。