Railsで使うデータベース上の特別な意味を持つ列名 その参 type
前回、前々回に続いて、RailsのMagic Field Namesです。
今回は"type"というフィールドについて。
RailsのWikiによると
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つのパターンを提唱しているそうです。
上のリンクの絵を見てもらえば、大体は分かると思います。
"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を知らないと、意味無いよ。