Ruby 1.9からDBIを使ってPostgreSQLに接続しようとした.postgresモジュールを改造してコネクトまではできたが,クエリーの発行は失敗に終わった.※なお,その後成功しました.その話はこちらです.このページの話は結局,不毛な試行錯誤ということになります (;´Д`)

Ruby1.9からDBIでPostgreSQL接続...失敗

最近,Rubyを始めた(ver 1.9).perlの時にDBIを便利に使っていたので, RubyでもDBIを使ってPostgreSQLに接続しようとした.が,苦難の道のりで結局実現できなかった.
  • Cent OS 4.4
  • PostgreSQL 8.3.4
  • ruby 1.9.1p0 (2009-01-30 revision 21907) [i686-linux]
  • dbi-0.4.1
  • dbd-pg-0.3.7
  • postgres-0.7.9.2008.01.28

※その後,成功しました.成功編はこちら → Ruby1.9からDBIでPostgreSQLに接続
なのでこのページの話は結局,不毛な試行錯誤ということになります.実用の役には立たないと思います

1.DBIのインストール

から,以下をダウンロードした.

  • dbd-pg-0.3.7.tar.gz
  • dbi-0.4.1.tar.gz

DBIは,普通にインストールする.

$ tar zxf dbi-0.4.1.tar.gz
$ cd dbi-0.4.1
$ ruby setup.rb config
$ ruby setup.rb setup
$ su
# ruby setup.rb install

2.DBD:Pg

DBD:Pgだが,後述するようにドライバとしてpostgresモジュールを使うので,頭の部分を少し変更する.
dbd-pg-0.3.7/lib/dbd/Pg.rb
[変更前]
require 'pg'

[変更後]
require 'postgres'
※ここは,後日間違いだと判明した.DBD:Pgを使うためには,「pg」のままにしてruby-pgを使うのが本来は正しいと思われる.

後はDBIと同じように,インストールする

$ ruby setup.rb config
$ ruby setup.rb setup
$ su
# ruby setup.rb install

3.PostgreSQLのドライバ

これが難関.探したところ,以下の3種類が見つかった.
  • postgres-pr : Pure Rubyのドライバ
  • ruby-postgres : C言語のドライバ.2003年ぐらいで更新が止まっている?
  • postgres : C言語のドライバ.ソースを見ると実体はruby-postgresとよく似ているがそれより新しい.ruby-postgresの後継か?

今回は,C言語かつ比較的新しい「postgres」を使うことにした.
※その後,ruby-pgという有望なのが見つかった.これは後日.

(1) まずGemsではどうか

とりあえずGemsで試してみる.
# gem install postgres

Building native extensions.  This could take a while...
/usr/local/lib/ruby/site_ruby/1.9.1/rubygems/ext/builder.rb:48: warning: Insecure world writable dir /home/kamoi in PATH, mode 040777
ERROR:  Error installing postgres:
        ERROR: Failed to build gem native extension.

/usr/local/bin/ruby extconf.rb install postgres
extconf.rb:4:in `<main>': uninitialized constant PLATFORM (NameError)
ということで,extconfスクリプトが,エラーで駄目!

調べてみたが,

によると,このモジュールがRuby 1.9に対応していないという場合のエラーらしい. そこでこのページを参考にして,1.9でも使えるように改造することにした.

(2) gemからファイルを取り出す

$ gem unpack postgres-0.7.9.2008.01.28.gem
改造するので,gemsの使用はもはやあきらめる.

(3) extconf.rb を編集する

「PLATFORM」の箇所を「RUBY_PLATFORM」に変更する(2箇所).これでextconf.rbは動くようになった
$ cd postgres-0.7.9.2008.01.28/ext
$ vi extconf.rb 〜
$ ruby extconf.rb --with-pgsql-include=/usr/local/pgsql/include \
  --with-pgsql-lib=/usr/local/pgsql/lib
$ make
しかしmakeで,コンパイルエラーが多発...こっからはC言語の問題だな.

(4) postgres.c を編集する

上記のページによると,ruby 1.9では配列データ構造体(RArray)の定義が変わったようで,コンパイルエラーはどうもそれが原因. メンバptr,lenを使わずにマクロ(RARRAY_LEN,RARRAY_PTR)を使うように修正すれば,コンパイルエラーは無くなった.
$ make
# make install
make installまで成功したので,irbから実際に動かしてみる.localhostにtestというデータベースがあるという前提.
$ irb
irb(main):001:0> require 'dbi'
=> true
irb(main):002:0> DBI.connect('DBI:Pg:test:localhost', 'postgres', '')

DBI::InterfaceError: Could not load driver (undefined local variable or method `e' for DBI:Module)
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbi.rb:312:in `rescue in load_driver'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbi.rb:236:in `load_driver'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbi.rb:154:in `_get_full_driver'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbi.rb:139:in `connect'
    from (irb):2
    from /usr/local/bin/irb:12:in `<main>'
エラー! orz.ドライバーをロードできないということで,まだpostgres.cに問題が残っている模様.

しかしこのエラーメッセージだけでは現象がさっぱりわからないので,Ruby側(DBD:Pg)とC側(postgres.c)の両方に,トレース出力を仕込んで原因を調べた. Ruby側は「p」,C側は「fprintf(stderr, 〜)」で,画面に表示することができた.

その結果,C側でPostgreSQLにクエリーを投げたときに,PostgreSQLから以下のエラーを受け取っていることがわかった

DBI::OperationalError: ERROR:  bind message supplies 1 parameters, but prepared statement "" requires 0
具体的には,バインドパラメータ(「?」文字)が無いSQLなのに, libpqのPQexec()関数ではなくPQexecParams()関数を使ってPostgreSQLにクエリーを投げているため,怒られているようだ.

なぜこんなことになるかというと,どうもC側の pgconn_exec(int argc, VALUE *argv, VALUE obj) 関数をRubyから呼び出すときに,呼び出し側と受け取り側で,引数の解釈が合ってないようだ.

呼び出し側は,
dbd-pg-0.3.7/lib/dbd/pg/database.rb

def _exec(sql, *parameters) ←可変引数で配列受け取り
    @pgexec.exec(sql, parameters)
end
dbd-pg-0.3.7/lib/dbd/pg/exec.rb
def exec(sql, parameters = nil)
    @pg_conn.exec(sql, parameters) ←sql文字列と,パラメータ配列を渡す
end
なので, pgconn_exec(int argc, VALUE *argv, VALUE obj) に渡ってくる引数はこうなる.(参考:Rubyのソース付属のREADME.EXT.ja)
引数内容
argc常に2
argv[0]SQL
argv[1]バインドパラメータ(Ruby配列)
objself(とりあえず今回はどうでも良い)

しかし,postgres.c側での受け方は,

static VALUE
pgconn_exec(argc, argv, obj)
    int argc;
    VALUE *argv;
    VALUE obj;
{
    ・・・
   rb_scan_args(argc, argv, "1*", &command, &params);
    ・・・
    if (RARRAY_LEN(params) <= 0) {
        result = PQexec(conn, StringValuePtr(command));
    }
となっており,呼び出しているrb_scan_args()関数の実装をRuby本体のソース(class.c)から見てみると, 受け取る引数が以下の構成でないと,誤動作するように見える
引数内容
argcバインドパラメータの個数+1
argv[0]SQL
argv[1]バインドパラメータ(1)
argv[2]バインドパラメータ(2)
argv[n]バインドパラメータ(n)
objself(とりあえず今回はどうでも良い)

何かが違う...この不整合を直すには,rb_scan_args()関数の呼び出し方を,

rb_scan_args(argc, argv, "11" , &command, &params);
にすれば良さそうだ.こうすれば,
  • argv[0]:SQL → command
  • argv[1]:バインドパラメータのRuby配列 → params

という風に,pgconn_exec()内の変数に正しく格納される.

他にも実行時エラーが出たので修正して,何とかDBI.connect()は通るようになった.ふぅ

一応ここまでの結果のパッチ(実は役に立たないと判明するのだが)

(5) 挫折

気をよくして,クエリーを投げるプログラムを走らせてみた. (localhostのtestデータベースに,「partition_manage」というテーブルがあるという前提)

test.rb

#!/usr/local/bin/ruby

require "dbi"

dbh = DBI.connect('DBI:Pg:test:localhost', 'postgres', '')
sth = dbh.execute("SELECT * FROM partition_manage")
rows = sth.fetch_all
col_names = sth.column_names
sth.finish
DBI::Utils::TableFormatter.ascii(col_names, rows)
dbh.disconnect if dbh
すると,
$ ./test.rb
/usr/local/lib/ruby/site_ruby/1.9.1/dbd/pg/exec.rb:19:in `prepare': undefined method `prepare' for #<PGconn:0x85432bc> (NoMethodError)
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbd/pg/database.rb:314:in `_prepare'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbd/pg/statement.rb:135:in `internal_prepare'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbd/pg/statement.rb:47:in `execute'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbi/base_classes/database.rb:96:in `execute'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbi/base_classes/database.rb:114:in `do'
    from /usr/local/lib/ruby/site_ruby/1.9.1/dbi/handles/database.rb:102:in `do'
    from ./test.rb:8:in `<main>'
というエラー...

これは,クエリーをexecuteしたときに,DBD:Pgの内部(exec.rb)でpostgresドライバのprepareというメソッドを実行しようとして, メソッドがなかったというエラーだ.

確かに,postgresドライバのソース(postgres.c)を見ると,prepareというメソッドは無い. (というか,rb_define_method()していない)

糸冬 了 ! orz

え〜,今後の策としては,

  • prepareメソッドを呼び出さないように,DBD:Pgを改造する →消極的対策
  • postgres.cに,以下のメソッドを追加実装する →積極的対策
    1. exec_prepared(stmt_name, parameters)
    2. prepare(stmt_name, sql)

のどちらかだろうか.どっちにしても苦労するなぁ...トホホ
っていうか,DBD:Pgは,一体どういうドライバとの組み合わせを想定しているのでございましょうか

※その後,ruby-pgという有望なのが見つかった.これには何とprepareもexec_preparedも含まれているので,これを1.9でもコンパイルできるように修正すれば,DBIでPostgreSQLに接続するという目的を達成できるかも知れない.


© 2024 KMIソフトウェア