DBIでbindした後のSQL文を引っ張り出す方法

会社で隣の席にすわってるKさんから「DBIでexecuteする時に色々な値をbindするじゃないですか。その後で実際に投げられたSQL文を引っ張ってくる方法ないんですか?」と質問されちゃいました。

そう言えばDBIx::Classとかだと環境変数でなんかセットするんだったけど、素のDBIの場合どうするのかな?と思って調べてみました。

DBIの長い長いドキュメントをずーっと読み進んで行くと、下の方に「TRACING」というセクションがありまして、そこにヒントが隠されてました。

どうやらTrace Flagsで「SQL」と指定すると標準エラーにトレース情報が出力されるみたいです。
具体的にはこれでいいみたい。

$dbh->{TraceLevel} = "3|SQL";

これでSTDERRにバコバコっとトレース情報が出力されて行きます。
STDERRの中に、実際に投げられたSQL文も含まれているので、これを正規表現で引っこ抜きます。

ベタに書くとこんなですね。

use DBI;
use strict;

my $dsn  = 'dbi:mysql:database:localhost';
my $user = 'hoge';
my $pass = 'fuga';

my $dbh  = DBI->connect($dsn, $user, $pass);

# 標準エラーを変数に置き換えておく
my $stderr;
close STDERR;
open STDERR, '>', \$stderr;

# トレースフラグを設定
$dbh->{TraceLevel} = "3|SQL";

my $sth  = $dbh->prepare("select * from table where col_A = ? and col_B = ? and col_C = ?");
$sth->execute('100', '30', '50');

# 正規表現でSQL分の箇所を抽出
$stderr =~ /Binding parameters:  ([^(\<\-)|(\-\-\>)]*)/sg;
my $sql = $1;
print $sql,"\n";

うーん、きたない。

実際には正規表現周りなんかはもっと色々書かなければいけないことがあるので、更にきたなくなります。

これだったら素直に自分でsql文にbind値をマップさせるような関数でも書いておいた方がなんぼかマシですね。

そもそもメインの処理の前後に、こんなにもガチャガチャ書かなければならないのはちょっと我慢できないぞ。

だいたいなんでDBIにもともとそういうメソッドが用意されていないんだ!

というわけで、DBI::Executedなるものを書いてみました。

use strict;
use DBI;
use DBI::Executed;

my $dsn  = 'dbi:mysql:database:localhost';
my $user = 'hoge';
my $pass = 'fuga';

my $dbh  = DBI->connect($dsn, $user, $pass);
my $sth  = $dbh->prepare("select * from table where col_A = ? and col_B = ? and col_C = ?");
$sth->execute('100', '30', '50');

print $dbh->executed_sql, "\n";

use DBI::Executedとしておくと、DBIにexecuted_sql()というメソッドが生えますので、SQL文を実行した後でこのメソッドを呼んであげればオKです。

簡単。すっきり。超きもちいい。


しかし、残念ながらいまのところmysqlでしか使えません。

STDERRに出力されるトレース情報ってDriverによって内容がまちまちで、正規表現で引っこ抜く部分はそれごとに書かなければダメっぽいです。

自分の環境がたまたまmysqlだったんで、今はmysqlだけしか対応できてません。
SQLiteも対応させようと思ったんですが、、、心が折れたので、なんとなく今のところはここら辺で放置。。。

ニーズがありそうだったら対応DBを増やして行こうかな。

codereposに置いてあるので、暇で暇で他にやることがない人がいたら、ぜひ拡張してあげてね!