Rails の Migration で MySQL の型を指定する
たとえば、こんな感じの Migration を考えてみる。
class CreatePepsi < ActiveRecord::Migration
def self.up
create_table :pepsies do |t|
t.column :coke, :string, :limit => 64
t.column :jolt, :integer
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
def self.down
drop_table :addresses
end
end
InnoDB の AUTO_INCREMENT とロック
きっかけは MySQL Users Conference Japan 2007 の`講演資料「新ストレージエンジン Falcon のアーキテクチャ詳細技術解説」(PDF) だった。
Falcon とは MySQL 6.0 で搭載予定の新しいストレージエンジンである。
この講演資料で Falcon が InnoDB より優れている点として、
AUTO_INCREMENT の割当にテーブルロックをかけない
があげられており、少々驚いた。
これはつまり、InnoDB の AUTO_INCREMENT がテーブルロックをかける、ということであり、そのことをいままで知らなかったからだ。
恥ずかしながら、AUTO_INCREMENT のスケーラビリティについては、いままであまり意識したことがなかった。
Rails で MySQL を使うときの注意点
Rails のブログでも取り上げられていた、"Rubyisms - MySQL-dump" が面白かったので、特に興味深かった一時テーブルまわりの要約を載せてみる。要約、というか読みながら書いたメモそのまんまですが。
- Rubyisms - MySQL-dump
- http://mysqldump.azundris.com/archives/72-Rubyisms.html
MySQL/Ruby の test.rb が失敗する理由と対策
前回書いた、MySQL/Ruby の test.rb が失敗する原因を調査する。
まずは、実行結果の出力を調べてみよう。 すべてのテストが失敗しているため長いので、最初のテスト結果のみ抜粋する。
% ruby ./test.rb localhost root newpass
...
1) Failure:
test_connect(TC_Mysql) [./test.rb:39]:
Exception raised:
Class: <Mysql::Error>
Message: <"Access denied for user 'ishikawa'@'localhost' (using password: NO)">
---Backtrace---
./test.rb:39:in `connect'
./test.rb:39:in `test_connect'
./test.rb:39:in `test_connect'
...
MySQL のエラーで <tt>"Access denied for user 'ishikawa'@'localhost' (using password: NO)"</tt> と出力されているので、これは単なるアクセス制御の問題だ。
そして、test.rb のコマンドライン引数で root ユーザを指定しているにも関わらず、実際には ishikawa ユーザでアクセスしようとしている。
うまくいくわけがない。 コマンドライン引数による指定が無視されているわけだ。
何故、コマンドライン引数が無視されるのか
コマンドライン引数が無視される原因を調べてみると、どうやら test.rb が使っている Test::Unit が悪さをしているらしい(Test::Unit は Ruby の標準添付ライブラリで、いわゆる xUnit ツールの Ruby 版である)。
簡単な検証スクリプトを書いてみる。
require "test/unit"
puts "before test: ARGV = #{ARGV.inspect}"
class SimpleTestCase < Test::Unit::TestCase
def test_argv()
puts "in test: ARGV = #{ARGV.inspect}"
end
end
ユニットテストが実行される前と、実行されるときに ARGV(コマンドライン引数の配列)をダンプするだけのスクリプトだ。これを適当な引数つきで実行して、その結果を確認する。
% ruby ./simple_test.rb 1 2 3
before test: ARGV = ["1", "2", "3"]
Loaded suite ./simple_test
Started
in test: ARGV =
.
Finished in 0.000316 seconds.
1 tests, 0 assertions, 0 failures, 0 errors
ユニットテストを実行する前では ARGV に引数の 1, 2, 3 が格納されているが、テストの内部では ARGV が空になっている。となると、Test::Unit が渡ってきた引数を食べてしまっているのに違いない。
Test::Unit のソースを調べる
実際のとこ、どうなっているのか。ソースコードに聞いてみよう。場所は /usr/local/lib/ruby/1.8/test/。まずは動作を確認するために、unit.rb の 276 行目だ。
at_exit do
unless $! || Test::Unit.run?
exit Test::Unit::AutoRunner.run
end
end
このコードは require されたときに実行され、at_exit で Test::Unit::AutoRunner の run メソッドが呼び出される。この定義は autorunner.rb だ。
module Test
module Unit
class AutoRunner
def self.run(force_standalone=false, default_dir=nil, argv=ARGV, &block)
r = new(force_standalone || standalone?, &block)
if((!r.process_args(argv)) && default_dir)
r.to_run << default_dir
end
r.run
end
怪しげなコードを発見できた。 インスタンスの process_args メソッドに ARGV をそのまま渡している。
def process_args(args = ARGV)
begin
options.order!(args) {|arg| @to_run << arg}
rescue OptionParser::ParseError => e
puts e
puts options
$! = nil
abort
else
@filters << proc{false} unless(@filters.empty?)
end
not @to_run.empty?
end
肝になるのは options.order!(args) の部分だ。options では、OptionParser インスタンスが返り、そして、OptionParser#order! はマニュアルによると、
与えられた argv を順番にパースします。 オプションではないコマンドの引数(下の例で言うと somefile)に出会うと、パースを中断します。 ブロックが与えられている場合は、パースを中断せずに 引数をブロックに渡してブロックを評価し、パースを継続します。argv を返します。
<strong>order! は与えられた argv を破壊的にパースします。 argv からオプションがすべて取り除かれます。</strong>
とのことなので、ここで ARGV の中身が破壊されているのは明白だ。
test.rb を変更して対策
では、どうするか?
結局、Test::Unit::AutoRunner.run で直接 ARGV を渡してしまうのが不味いわけで、自前で Test::Unit::AutoRunner.run を呼び出し、そのさいに ARGV のコピーを渡すようにしてみた。
--- test.rb.orig 2006-12-28 01:20:19.000000000 +0900
+++ test.rb 2006-12-28 01:21:04.000000000 +0900
@@ -1429,3 +1429,4 @@
end
end if Mysql.client_version >= 40100
+Test::Unit::AutoRunner.run(false, nil, ARGV.dup)
これでテストも実行できるはず ...
% ruby ./test.rb localhost root
Loaded suite ./test
Started
....................................................................FF...........................................
Finished in 0.32177 seconds.
通った!...
1) Failure:
test_fetch_double(TC_MysqlStmt2) [./test.rb:920]:
<-1.79769313486232e+308> and
<-1.79769313486232e+308> expected to be within
<2.22044604925031e-16> of each other.
2) Failure:
test_fetch_double_unsigned(TC_MysqlStmt2) [./test.rb:937]:
<1.79769313486232e+308> and
<1.79769313486232e+308> expected to be within
<2.22044604925031e-16> of each other.
... と思ったら駄目でした。
うーん、高速化パッチを適用せずにテストしても同じ結果なので、これはまた別の原因だな。 まあ、浮動小数点関係っぽいので環境依存かもしれん、、てことでスルーしとこう。
Mac OS X に MySQL/Ruby をインストール + 高速化パッチ
長かった Ruby on Rails 環境構築シリーズもやっと終盤です。
Ruby から MySQL につなぐためのバインディングである MySQL/Ruby をインストールすれば、Ruby on Rails の開発環境構築は一段落。
なお、同様のバインディングである Ruby/MySQL もあり、こちらは Ruby で書かれているためコンパイルが不要。MySQL/Ruby とも、ほぼ互換性がある。ただ、今回はパフォーマンス優先でいきたいと思う。
さて、ダウンロードページから最新版をダウンロード。現時点では mysql-ruby-2.7.3.tar.gz が最新版のようだ。
% curl --location -O http://tmtm.org/downloads/mysql/ruby/mysql-ruby-2.7.3.tar.gz % tar xvzf mysql-ruby-2.7.3.tar.gz % cd mysql-ruby-2.7.3
さきほど「パフォーマンス優先」と書いたけど、RailsExpress.blog の人が MySQL/Ruby を 30% 高速にするパッチを公開している。記事を読んだときから試してみたかったので、これを機会に使ってみよう。
パッチをダウンロードして、MySQL/Ruby の展開先に置いたら、patch コマンドでパッチを適用する。
% curl -O http://railsexpress.de/downloads/mysql-ruby-2.7-less-string-copies-in-each-hash.diff % patch < mysql-ruby-2.7-less-string-copies-in-each-hash.diff patching file mysql.c.in Hunk #1 succeeded at 1009 (offset -1 lines). Hunk #2 succeeded at 1027 (offset -1 lines). Hunk #3 succeeded at 1059 (offset -1 lines). Hunk #4 succeeded at 1136 (offset -1 lines). Hunk #5 succeeded at 2079 (offset 3 lines).
無事、パッチを適用できたようなので、そのままインストールに進む。extconf.rb を実行するときに、--with-mysql-dir オプションで MySQL のインストール先ディレクトリを指定する必要があった。
% ruby extconf.rb --with-mysql-dir=/usr/local/mysql checking for mysql_query() in -lmysqlclient... no checking for main() in -lm... yes checking for mysql_query() in -lmysqlclient... no checking for main() in -lz... yes checking for mysql_query() in -lmysqlclient... yes checking for mysql_ssl_set()... yes checking for mysql.h... no checking for mysql/mysql.h... yes creating Makefile
さて、コンパイル ...
% make ... mysql.c: In function 'Init_mysql': mysql.c:2018: error: 'ulong' undeclared (first use in this function)
怒られてしまった。ulong 型が定義されていないようだ。 検索してみて、ここに書かれている解決策を採用。ただし unsigned long ではなく u_long にしておく。
mysql.c の最初(16 行目くらい)に
#define ulong u_long
を追加してリトライ。
% make gcc -I. -I. -I/usr/local/lib/ruby/1.8/i686-darwin8.8.3 -I. -DHAVE_MYSQL_SSL_SET -DHAVE_MYSQL_MYSQL_H -I/usr/local/mysql/include -fno-common -g -O2 -pipe -fno-common -c mysql.c cc -dynamic -bundle -undefined suppress -flat_namespace -L"/usr/local/mysql/lib" -L"/usr/local/lib" -L"/usr/local/mysql/lib/mysql" -o mysql.bundle mysql.o -lmysqlclient -lz -lm -ldl -lobjc
無事コンパイルできた。 しかし、テストが通らない ...
% ruby ./test.rb localhost ishikawa password ... 113 tests, 44 assertions, 3 failures, 177 errors
うーむ。つづきは今度。