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を知らないと、意味無いよ。

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

前回Timestamping関係のカラムを試してみた。
今回はOptimistic Lockingだ。
列名は"lock_version"だ。
適当な、プロジェクトと、モデル、Scaffoldを作る。
モデルはDiaryという名で、こんな感じのmigrationファイル。

class CreateDiaries < ActiveRecord::Migration
  def self.up
    create_table :diaries do |t|
      t.column :title, :string
      t.column :text, :text
      t.column :lock_version, :integer
    end
  end

  def self.down
    drop_table :diaries
  end
end

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

mysql> desc diaries;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int(11)      | NO   | PRI | NULL    | auto_increment |
| title        | varchar(255) | YES  |     | NULL    |                |
| text         | text         | YES  |     | NULL    |                |
| lock_version | int(11)      | YES  |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+

"lock_version"がユーザにより入力されると調子が悪そうな気がするので、app/views/diaries/_form.rhtmlを編集。その前に、ちょっと読む。

Optimistic Locking

Active Records support optimistic locking if the field lock_version is present. Optimistic Locking requires that at least one item in the model requires uniqueness validation (e.g. validates_uniqueness_of :foo). If you don’t have validates_uniqueness_of you will run into call back errors.

Ensure that the attribute lock_version is passed in and can be evaluated between conflicting posts. i.e. put it in your view as a hidden field.

API documentation for Locking

む。hiddenフィールドで渡してほしいのか。じゃあ、

<%= error_messages_for 'diary' %>

<!--[form:diary]-->
<p><label for="diary_title">Title</label><br/>
<%= text_field 'diary', 'title'  %></p>

<p><label for="diary_text">Text</label><br/>
<%= text_area 'diary', 'text', :rows => 5 %></p>

<%= hidden_field 'diary', 'lock_version' %>
<!--[eoform:diary]-->

しかし!
記事を書いて、それをupdateしようとすると、nil.+ができない!NoMethodErroが出て、お亡くなりになる。
どやら、モデルのupdate_attributesで失敗しているようだ。
困った。
edit画面のHTMLソースを見るとlock_versionのvalueが無い。
MySQLのプロンプトから"lock_version"を見ると、NULLだ。
試行錯誤の結果、lock_versionにはデフォルト値を設定してやるとうまく動くようだ。
こんな感じにmaigrate。

mysql> desc diaries;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| id           | int(11)      | NO   | PRI | NULL    | auto_increment |
| title        | varchar(255) | YES  |     | NULL    |                |
| text         | text         | YES  |     | NULL    |                |
| lock_version | int(11)      | YES  |     | 1       |                |
+--------------+--------------+------+-----+---------+----------------+

これで、動くはずです。
一人二役IEFireFox(以下FX)を使って再現します。

  1. 何かしらの記事を作っておく。
  2. FXでEdit画面に行く。(Submitは、まだしない。)
  3. IEでEdit画面に行く。(Submitは、まだしない。)
  4. IEでSubmitし、更新を確定する。
  5. FXでSubmitし、更新を確定しようとする。

例外発生!FXでは更新できませんでした。
例外がドバーッと描画されるので、ハンドリングしてやります。
app/controllers/diaries_controller.rbを変更。

(略)
def update
  @diary = Diary.find(params[:id])
  begin
    if @diary.update_attributes(params[:diary])
      flash[:notice] = 'Diary was successfully updated.'
      redirect_to :action => 'show', :id => @diary
    else
      render :action => 'edit'
    end
  rescue ActiveRecord::StaleObjectError
    flash[:notice] = '何者かに先に書き込まれました。やり直してください。'
    @diary = Diary.find(params[:id])
    render :action => 'edit'
  end
end
(略)

MagicFieldNames in Ruby on Railsには、validates_uniqueness_ofを書かないと、call back errorsになるよ。っぽいことが書かれてあるような気がするんですが。書いてないけどcall back errorsは無い。idがプライマリーキーでユニークだからだろうか?
まぁいいや。

要点

"lock_version"の列にはデフォルトを設定してやるといいよ。

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

Railsをかじっていると、"id"やら、"created_at"やら便利なカラムの存在が気になってきました。
ほかにも色々と便利なカラムがあるけど、知らないから損をしているんじゃないかと。
で、調べてみた。
MagicFieldNames in Ruby on Rails
なんか、いっぱいある。

  • created_at
  • created_on
  • updated_at
  • updated_on
  • lock_version
  • type
  • id
  • #{table_name}_count
  • position
  • parent_id
  • lft
  • rgt

実験

とりあえず、Timestampingと題してある、

  • created_at
  • created_on
  • updated_at
  • updated_on

から調べてみよう。
適当にプロジェクトと、モデルを作成。

>rails MagicField
(略)
>cd MagicField
>ruby script\generate model diary
(略)

んで、migration作成。(MySQL、config/database.ymlなど適当にセットアップ)
db/migrate/001_create_diaries.rb

(略)
def self.up
  create_table :diaries do |t|
    t.column :title, :string
    t.column :text, :text
    t.column :created_at, :datetime
    t.column :created_on, :datetime
    t.column :updated_at, :datetime
    t.column :updated_on, :datetime
  end
end
(略)

で、migrate

>rake db:migrate

テーブルを確認。MySQLのプロンプトから

mysql> desc diaries;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255) | YES  |     | NULL    |                |
| text       | text         | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| created_on | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
| updated_on | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

動くものが見たいので、Scaffoldも作っちゃう。

>ruby script\generate scaffold diary

Scaffoldのままだと、フォームにcreated_at、created_on、updated_at、updated_onも含まれてしまうので、app/views/diaries/_form.rhtmlのTimestamping関係を削除。あと、Textのフォームが馬鹿でかいので調整。
調整後の、app/views/diaries/_form.rhtm全文。

<%= error_messages_for 'diary' %>

<!--[form:diary]-->
<p><label for="diary_title">Title</label><br/>
<%= text_field 'diary', 'title'  %></p>

<p><label for="diary_text">Text</label><br/>
<%= text_area 'diary', 'text', :rows => 5 %></p>
<!--[eoform:diary]-->

HTTPサーバ起動

>ruby script\server


で、書いてみる。


編集してみる。


"created"と"updated"は読んで字のごとく、作られた時刻と更新された時刻だ。
"at"と"on"の違いがわかりません。仕方ない、読むか。

  • created_on: date only
  • created_at: date and time
  • same for updated_at/updated_on fields

leeo: While this is a commonly held convention, I don’t see it referenced in the Rails source anywhere. More importantly, at/on doesn’t actually affect the value or type of data being stored. Rails simply throws a Time in there, which gets cast into a Date if so required by the schema.

むむ、スキーマがそう望むなら時間を含まない日付になる?
テーブルを変えてみよう。

>ruby script\generate migration change_on

db/migrate/002_change_on.rb

(略)
def self.up
  change_column(:diaries, :created_on, :date)
  change_column(:diaries, :updated_on, :date)
end
(略)

で、migrate

>rake migrate

diariesテーブルはこんな感じ。

mysql> desc diaries;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255) | YES  |     | NULL    |                |
| text       | text         | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| created_on | date         | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
| updated_on | date         | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

で、作成&編集。

なるほど。
逆に"at"を:dateにして実験。

mysql> desc diaries;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255) | YES  |     | NULL    |                |
| text       | text         | YES  |     | NULL    |                |
| created_at | date         | YES  |     | NULL    |                |
| created_on | date         | YES  |     | NULL    |                |
| updated_at | date         | YES  |     | NULL    |                |
| updated_on | date         | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+


動いた…
"at"と"on"の違いは、それを人が読んだ時にどう感じるかで、実装上は同じ処理なのだろうか…

URIとCGIのescape

最近、さまざまなWebサービスがある。
amazonだったり、Yahoo!だったり。
そこで、Rubyを使い便利ツールを作って遊んでいるのだが、RESTでリクエストする場合はエスケープしてやらないといけない。
エスケープしてくれる人はいないかと探したところ、URI.escapeとCGI.escapeの二人がエスケープしてくれる模様だ。
まずURI.escape

URI.escape(str[, unsafe])
URI.encode(str[, unsafe])
URI 文字列をエンコードした文字列を返します。unsafe には、URI として指定できない文字を正規表現か文字列で指定します(デフォルトは、定数 URI::UNSAFE
/[^-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n
です)。

どうやら、デフォルトではURIとして使ってはいけない文字をエスケープしてくれるようだ。
しかし、"?"や"&"などWebサービスにとって意味のある文字はスルーされる。
続いて、CGI.escape

CGI.escape(string)
string を URL エンコードした文字列を新しく作成し返します。

具体的に何をエスケープしてくれるのか書いてないので、ソースを見てみる。

# cgi.rb - cgi support library
# 
# Copyright (C) 2000  Network Applied Communication Laboratory, Inc.
# 
# Copyright (C) 2000  Information-technology Promotion Agency, Japan
#
# Author: Wakou Aoyama <wakou@ruby-lang.org>
中略
  # URL-encode a string.
  #   url_encoded_string = CGI::escape("'Stop!' said Fred")
  #      # => "%27Stop%21%27+said+Fred"
  def CGI::escape(string)
    string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
      '%' + $1.unpack('H2' * $1.size).join('%').upcase
    end.tr(' ', '+')
  end

こちらは"?"や"&"もエスケープしてくれる模様だ。

結論

各種Webサービスを使うときに、クエリーとなる文字列をエスケープする場合はCGI.escapeを使うべし。

Railsのログを読む

RailsのDatabase関係(ActiveRecord)のログは色つきで吐かれるそうです。
こんな感じ

  SQL (0.000000)   SET NAMES 'utf8'
  SQL (0.300000)   CREATE TABLE schema_info (version int(11))
  SQL (0.150000)   INSERT INTO schema_info (version) VALUES(0)
  SQL (0.000000)   Mysql::Error: #42S01Table 'schema_info' already exists: CREATE TABLE schema_info (version int(11))
  SQL (0.020000)   SELECT version FROM schema_info

UNIX系?Linux系?のOSだと"lv"というコマンドで綺麗に見れるそうです。
Windows系など"lv"が無いと見づらいだけなので、色付けをOFFにします。

ついでに、ログに色をつける設定をやめる。
テキストエディタで見たときに化けるので。

ActiveRecord::Base.colorize_logging = false

これを"config/environment.rb"へ書きます。
脱色後はこんな感じ

SQL (0.000000)  SET NAMES 'utf8'
SQL (0.000000)  Mysql::Error: #42S01Table 'schema_info' already exists: CREATE TABLE schema_info (version int(11))
SQL (0.000000)  Mysql::Error: #42S01Table 'schema_info' already exists: CREATE TABLE schema_info (version int(11))
SQL (0.010000)  SELECT version FROM schema_info


しかし、WinXPだとLinuxのtailコマンドのようなログを見る便利なヤツがデフォルトでは存在しません。
メモ帳で見てもリアルタイムにログの更新を追いかけてくれないので、不便です。
そこで、Windows Server 2003 Resource Kit Tools
これで、tailコマンドが使えます。
Windows Server 2003 Resource Kit Toolには他にも色々とツールが含まれているそうですが、知りません。tailが使えればそれでいいんです。

英語の複数形は「ス」?「ズ」?

いまさらながら、Ruby on Railsのレールに乗ろうと勉強しています。
RailsのConvention over Configurationによるとデータベースのテーブルは複数形にします。
複数形化はActiveSupportにより、Stringクラスが拡張され、String.pluralizeで行えます。

C:\>irb -r rubygems -r active_support
irb(main):001:0> 'convention'.pluralize
=> "conventions"
irb(main):002:0> 'configuration'.pluralize
=> "configurations"
irb(main):003:0> 'person'.pluralize
=> "people"

personがpeopleになったりと、personsとしそうな人にとってはpluralizeがいい感じです。
ただ、英会話が非日常な私にとって、conventionsって「コンベンションス」?「コンンベンションズ」?
確か、中学ぐらいで「〜ス」「〜ズ」の違いを習った気がするけど忘れました。
で、調べてみました。

★1 無声子音+S books cups
★2 有声子音+Z dogs cabs fools
★2 母音+Z zoos
★例外 S+IZ buses kisses

なるほど、conventionsは「コンベンションズ」、Railsは「レイルズ」ですね。