MySQL with Ruby


5月の連休に入って、連休でなければ出来ない事をしている。髭を伸ばしているのである。最初は休みだから髭なんて剃らなくいいやと、いわゆる不精髭だったのであるが、女房が嫌うので、口の上だけを残してみた。いわゆる口髭というやつである。女房は似合うと言ってくれるのだが、本人はまぬけ顔がよけいまぬけに見えるようで、大いに恥ずかしい。出社前日にスパッと剃る事にしよう。

■ MySQL

MySQL本がオライリーから出版されたのを記念してRubyとの融合をやってみる事にします。思えば以前には、perlとの融合をやっていましたので久しぶりのMySQLです。正直、すっかりSQLを忘れてしまってますので、復習を兼ねています。

FreeBSDですと、MySQLが既にportsになっていますので簡単にインストールできるはずですが、ソースから頑張って入れてみます。mysql-3.22.32を取ってきました。configure一発なのですが、 --with-charset=ujisを忘れないようにします。非力なマシンだとついでに、--with-low-memoryも付けておきます。でも、memori(swap)をゴージャスに要求されますので、覚悟してください。

苦労の上無事インストールが出来たとしましょう。後は本なりマニュアルを読みながら設定をしていきます。私は、最終的にwebからアクセスしたいので次のようにしました。

管理用DBであるmysqlに接続して、ユーザー(nobody)とそのパスワード(password)を設定しました。(権利は何でも出来るゆるゆる設定です)

[sakae@atom]$ mysql --user=root mysql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2 to server version: 3.22.32

Type 'help' for help.

mysql> GRANT ALL PRIVILEGES ON *.* TO nobody@localhost
    -> IDENTIFIED by 'password' WITH GRANT OPTION;
Query OK, 0 rows affected (0.02 sec)

mysql> GRANT ALL PRIVILEGES ON *.* TO nobody@"%"
    -> IDENTIFIED by 'password' WITH GRANT OPTION;
Query OK, 0 rows affected (0.01 sec)

mysql> select host,user,password,Create_priv,Grant_priv from user where user='nob
ody';
+-----------+--------+------------------+-------------+------------+
| host      | user   | password         | Create_priv | Grant_priv |
+-----------+--------+------------------+-------------+------------+
| localhost | nobody | 5d2e19393cc5ef67 | Y           | Y          |
| %         | nobody | 5d2e19393cc5ef67 | Y           | Y          |
+-----------+--------+------------------+-------------+------------+
2 rows in set (0.00 sec)

次に出来立てのユーザーでDBにアクセスしてみます。
[sakae@atom]$ mysql -u nobody mysql
ERROR 1045: Access denied for user: 'nobody@localhost' (Using password: NO)
おっとパスワードの入力を忘れたため文句を言われました。やり直し
[sakae@atom]$ mysql -u nobody -ppassword mysql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

今度はうまくいきました。

rubytestという新たなDBを作ります。
mysql> CREATE DATABASE rubytest;
Query OK, 1 row affected (0.01 sec)

mysql> use rubytest;
Database changed
mysql> show tables;
Empty set (0.01 sec)
まだテーブルは有りません。

しょうがないので、マニュアルからテーブルの例をぱくってきます。ペットのテーブルです。

mysql> CREATE TABLE pet (name VARCHAR(20), owner VARCHAR(20),
    -> species VARCHAR(20), sex CHAR(1), birth DATE, death DATE);
Query OK, 0 rows affected (0.02 sec)

mysql> show tables;
+--------------------+
| Tables in rubytest |
+--------------------+
| pet                |
+--------------------+
1 row in set (0.00 sec)

mysql> DESCRIBE pet;
+---------+-------------+------+-----+---------+-------+
| Field   | Type        | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| name    | varchar(20) | YES  |     | NULL    |       |
| owner   | varchar(20) | YES  |     | NULL    |       |
| species | varchar(20) | YES  |     | NULL    |       |
| sex     | char(1)     | YES  |     | NULL    |       |
| birth   | date        | YES  |     | NULL    |       |
| death   | date        | YES  |     | NULL    |       |
+---------+-------------+------+-----+---------+-------+
6 rows in set (0.01 sec)

これで入れ物が出来ました。次は中身です。pet.txtというプレーンファイルにデータを用意します。フィールドのセパレータはTABです。内容の無い所は、\N を入れておきます。

----- pet.txt ------------------------------------
name    owner   species sex     birth   death
Fluffy  Harold  cat     f       1993-02-04
  :
ブ−    佐藤    pig     f       1993-11-14
デメ    森      fish    \N      2000-02-25
------------------------------------------------

mysql>  LOAD DATA LOCAL INFILE "pet.txt" INTO TABLE pet;
Query OK, 13 rows affected (0.01 sec)
Records: 13  Deleted: 0  Skipped: 0  Warnings: 13

mysql> select * from pet;
+----------+--------+---------+------+------------+------------+
| name     | owner  | species | sex  | birth      | death      |
+----------+--------+---------+------+------------+------------+
| name     | owner  | species | s    | 0000-00-00 | 0000-00-00 |
| Fluffy   | Harold | cat     | f    | 1993-02-04 | NULL       |
| Claws    | Gwen   | cat     | m    | 1994-03-17 | NULL       |
| Buffy    | Harold | dog     | f    | 1989-05-13 | NULL       |
| Fang     | Benny  | dog     | m    | 1990-08-27 | NULL       |
| Bowser   | Diane  | dog     | m    | 1998-08-31 | 1995-07-29 |
| Chirpy   | Gwen   | bird    | f    | 1998-09-11 | NULL       |
| Slim     | Benny  | snake   | m    | 1996-04-29 | NULL       |
| 黒       | 小林   | cat     | m    | 1988-04-01 | 1992-10-14 |
| ピ−     | 佐々木 | bard    | f    | 1997-06-23 | NULL       |
| トラ     | 山口   | cat     | f    | 1998-08-13 | NULL       |
| ブ−     | 佐藤   | pig     | f    | 1993-11-14 | NULL       |
| デメ     | 森     | fish    | NULL | 2000-02-25 | NULL       |
+----------+--------+---------+------+------------+------------+
13 rows in set (0.01 sec)

ちゃんと読み込めているようですが、タイトルまで登録されちゃいました。消しておきましょう。

mysql> delete from pet where name='name';
Query OK, 1 row affected (0.00 sec)

mysql>  select * from pet order by birth;
+----------+--------+---------+------+------------+------------+
| name     | owner  | species | sex  | birth      | death      |
+----------+--------+---------+------+------------+------------+
| 黒       | 小林   | cat     | m    | 1988-04-01 | 1992-10-14 |
| Buffy    | Harold | dog     | f    | 1989-05-13 | NULL       |
| Fang     | Benny  | dog     | m    | 1990-08-27 | NULL       |
| Fluffy   | Harold | cat     | f    | 1993-02-04 | NULL       |
| ブ−     | 佐藤   | pig     | f    | 1993-11-14 | NULL       |
| Claws    | Gwen   | cat     | m    | 1994-03-17 | NULL       |
| Slim     | Benny  | snake   | m    | 1996-04-29 | NULL       |
| ピ−     | 佐々木 | bard    | f    | 1997-06-23 | NULL       |
| トラ     | 山口   | cat     | f    | 1998-08-13 | NULL       |
| Bowser   | Diane  | dog     | m    | 1998-08-31 | 1995-07-29 |
| Chirpy   | Gwen   | bird    | f    | 1998-09-11 | NULL       |
| デメ     | 森     | fish    | NULL | 2000-02-25 | NULL       |
+----------+--------+---------+------+------------+------------+
12 rows in set (0.01 sec)
 
ふう、長かったですがDBの準備が整いました。

■ mysql-ruby

mysql-ruby-2.2.0をRAAから頂いてきました。インストールは簡単なので省略します。まずは、ちゃんと動くか確認。

--- checkdb.rb ---------------------------------------------
#!/usr/local/bin/ruby
require "mysql"
m = Mysql.new('localhost', 'nobody', 'password', 'rubytest')
res = m.query("select * from pet")
fields = res.fetch_fields.filter do |f| f.name end
puts fields.join("\t")
res.each do |row|
    puts row.join("\t")
end
m.close
------------------------------------------------------------

[sakae@atom]$ ./checkdb.rb
name    owner   species sex     birth   death
Fluffy  Harold  cat     f       1993-02-04
Claws   Gwen    cat     m       1994-03-17
Buffy   Harold  dog     f       1989-05-13
Fang    Benny   dog     m       1990-08-27
Bowser  Diane   dog     m       1998-08-31      1995-07-29
Chirpy  Gwen    bird    f       1998-09-11
Slim    Benny   snake   m       1996-04-29
黒      小林    cat     m       1988-04-01      1992-10-14
ピ−    佐々木  bard    f       1997-06-23
トラ    山口    cat     f       1998-08-13
ブ−    佐藤    pig     f       1993-11-14
デメ    森      fish            2000-02-25

どうやら大丈夫そうです。次は、Webからです。本当は、cgiで検索条件等を入力出来るようにするのが筋なのでしょうが手抜きして、アクセス出来るかどうかのチェックをします。

--- sample.rb ----------------------------------------------
#!/usr/local/bin/ruby
require "mysql"
require "cgi-lib.rb"

def test_db
          out = ""
          m = Mysql.new('localhost', 'nobody', 'password', 'rubytest')
          res = m.query("select * from pet")
          fields = res.fetch_fields.filter do |f| f.name end 
               out << fields.join("\t") << "\n"
          res.each do |row|
               out << row.join("\t") << "\n"
          end
          m.close
          return out
end

CGI::print{
    CGI::tag("HTML"){
        CGI::tag("HEAD"){ CGI::tag("TITLE"){"Test MySQL"} } +
        CGI::tag("BODY"){
            CGI::tag("H2"){"MySQL Access Test from Web"} +
            CGI::tag("HR") +
                    CGI::tag("PRE"){ test_db }  +
            CGI::tag("HR")
       }
    }
}
------------------------------------------------------------

[sakae@atom]$ w3m -dump http://atom/cgi-bin/sample.rb

MySQL Access Test from Web

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
name    owner   species sex     birth   death
Fluffy  Harold  cat     f       1993-02-04      
Claws   Gwen    cat     m       1994-03-17      
Buffy   Harold  dog     f       1989-05-13      
Fang    Benny   dog     m       1990-08-27      
Bowser  Diane   dog     m       1998-08-31      1995-07-29
Chirpy  Gwen    bird    f       1998-09-11      
Slim    Benny   snake   m       1996-04-29      
黒      小林    cat     m       1988-04-01      1992-10-14
ピ−    佐々木  bard    f       1997-06-23      
トラ    山口    cat     f       1998-08-13      
ブ−    佐藤    pig     f       1993-11-14      
デメ    森      fish            2000-02-25      
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ちゃんとアクセスしてくれています。気になるスピードですが

[sakae@atom]$ /www/bin/ab -n 500 http://atom/cgi-bin/sample.rb
This is ApacheBench, Version 1.3c <$Revision: 1.38 $> apache-1.3
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-1999 The Apache Group, http://www.apache.org/

Server Software:        Apache/1.3.12                                      
Server Hostname:        atom
Server Port:            80

Document Path:          /cgi-bin/sample.rb
Document Length:        526 bytes

Concurrency Level:      1
Time taken for tests:   30.544 seconds
Complete requests:      500
Failed requests:        0
Total transferred:      328000 bytes
HTML transferred:       263000 bytes
Requests per second:    16.37
Transfer rate:          10.74 kb/s received

Connnection Times (ms)
              min   avg   max
Connect:        0     0    12
Processing:    51    60   897
Total:         51    60   909
[sakae@atom]$ 

本当はmod_rubyでも使えば格段にスループットが上がると思うのですが、

[Wed May  4 21:27:49 2000] [error] ruby script error
/www/cgi-bin/sample.rb:2:in `require': /usr/local/lib/ruby/1.4/i386-freebsd4.0/mysql.so: Undefined symbol "rb_eTypeError" - /usr/local/lib/ruby/1.4/i386-freebsd4.0/mysql.so (LoadError)
        from /www/cgi-bin/sample.rb:2

エラーになってしまい、使えませんでした。グスン。多分mod_rubyが使えれば3秒ぐらいで回るのではないかと思います。

■バッチスクリプト

MySQL本が出たと言うので東京まで出かけたのですが、結局その本は買わずにカーネル本等を仕入れてしまいました。こりゃいかんと言う訳で、オープンデザイン(No.38)を地元で買っちゃいました。rubyの連載とperlによるWebプログラミングなんて記事が出ていたからです。この記事では、Postgress+DBI,DBDで解説されてましたので、rubyでやったらどうなるのとヘソを曲げてみたのでした。

記事の中にはバッチスクリプトを書いて、テキストデータをDBに入れる紹介がありましたので、真似てみました。どうせ作るなら実用になりそうなものを(あくまで、私にとってですが)書いてみようという事で、次のような状況を想定してます。

大型ホストから出て来るDBのデータは、固定長のファイルになっている。これをMySQLに移そうという訳です。固定長でフィールドが分割(?)されてますので、そのフィールド名や、長さ、MySQLでのフィールドのタイプを定義したスペックファイルを用意して、それを元にデータを移す事にします。

--- spec file -----------------------
e       8       VARCHAR(8)
k       12      VARCHAR(12)
g       10      VARCHAR(10)
u       17      VARCHAR(17)
d       8       INTEGER
-----------------------------------

左からフィールド名、長さ、MySQLのタイプをTAB区切りで書いたファイルです。フィールド名と長さはそのまま大型ホストのDB仕様書から貰ってくればいいでしょう。(フィールド名は実名にすると差障りがあるのでイニシャル文字にしてます) スクリプトは次のようなものです。一時ファイルを作って一気読みでMySQLへ持って行きます。

--- batch.rb --------------------------------------------------------
#!/usr/local/bin/ruby
require "mysql"

$SPEC_FILE="spec"               # table spec file
$DAT_FILE="dat.txt"             # fixed length data file
$TABLE_NAME="smpl"              # table name

$m = Mysql.new('localhost', 'nobody', 'password', 'rubytest')

def mk_format_data
        fmt = ""
        t_spec = Array.new()
        File.foreach( $SPEC_FILE ){ |ll|
                ll.chomp!
                field,len,type=ll.split( "\t" )
                fmt += "A#{len}"                # for unpack template
                t_spec << "#{field} #{type}"    # for create table
        }
        return fmt, t_spec.join(",\n")
end

def del_table
        $m.query( "DROP TABLE #{$TABLE_NAME} \n" )
end

def mk_table(tbl_spec)
        $m.query( "CREATE TABLE #{$TABLE_NAME} ( #{tbl_spec} )\n" )
end

def ins_data(fmt)
        f=File.open( "#{$$}", "w" )             # temp file (tsv format)
        File.foreach( $DAT_FILE ){ |l|
                l.chomp!
                d=l.unpack( "#{fmt}" )
                f.puts d.join( "\t" )
        }
        f.close
        $m.query( %Q!LOAD DATA LOCAL INFILE "#{$$}" INTO TABLE #{$TABLE_NAME}\n! )
        File.unlink( "#{$$}" )
end

fmt,tbl_spec = mk_format_data()
## del_table()  ##### if you need
mk_table(tbl_spec)
ins_data(fmt)
$m.close
---------------------------------------------------------------------------

[sakae@atom]$ wc dat.txt 782
   42029  232579 2353624 dat.txt
   42029  238167 1740310 782

上記は、一時ファイルを消さないようにしてwcしてみたものです。4万行以上あるデータですが

[sakae@atom]$ time ./batch.rb 

real    0m3.086s
user    0m2.394s
sys     0m0.149s

かなりのスピードで処理が終っています。本当にデータが入ったのでしょうか? ちょっと調べてみます。

mysql> DESCRIBE smpl;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| e     | varchar(8)  | YES  |     | NULL    |       |
| k     | varchar(12) | YES  |     | NULL    |       |
| g     | varchar(10) | YES  |     | NULL    |       |
| u     | varchar(17) | YES  |     | NULL    |       |
| d     | int(11)     | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
5 rows in set (0.01 sec)

mysql> select count(e) from smpl;
+----------+
| count(e) |
+----------+
|    42029 |
+----------+
1 row in set (0.23 sec)

大丈夫そうですね。所で、ruby-mysqlにはDBIがサポートしているプレースホルダの機能は無いのでしょうか? 何でもqueryに突っ込んじゃえばいいので、まどろっこしい事はするなと言う事なのでしょう。深く考えない事にします。