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を知らないと、意味無いよ。
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.
む。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 | | +--------------+--------------+------+-----+---------+----------------+
これで、動くはずです。
一人二役をIEとFireFox(以下FX)を使って再現します。
- 何かしらの記事を作っておく。
- FXでEdit画面に行く。(Submitは、まだしない。)
- IEでEdit画面に行く。(Submitは、まだしない。)
- IEでSubmitし、更新を確定する。
- 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 onlycreated_at
: date and time- same for
updated_at/updated_on
fieldsleeo: 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 | | +------------+--------------+------+-----+---------+----------------+
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
こちらは"?"や"&"もエスケープしてくれる模様だ。
■
はてなパーカー欲しい!
なんか、欲しくなりますよね。
IT関連企業のグッズって。
Railsのログを読む
RailsのDatabase関係(ActiveRecord)のログは色つきで吐かれるそうです。
こんな感じ
[4;36;1mSQL (0.000000)[0m [0;1mSET NAMES 'utf8'[0m [4;35;1mSQL (0.300000)[0m [0mCREATE TABLE schema_info (version int(11))[0m [4;36;1mSQL (0.150000)[0m [0;1mINSERT INTO schema_info (version) VALUES(0)[0m [4;35;1mSQL (0.000000)[0m [0mMysql::Error: #42S01Table 'schema_info' already exists: CREATE TABLE schema_info (version int(11))[0m [4;36;1mSQL (0.020000)[0m [0;1mSELECT version FROM schema_info[0m
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は「レイルズ」ですね。