DBI で printf オレオレ流

Kazuho@Cybozu Labs: DBI::Printf - A Yet Another Prepared Statement を読んでおもしろ,と思ったんですが,どうもこれを prepared statement と呼ぶことに抵抗感があった*1のでコメントしたら,丁寧にコメントを返してくださいました。感謝感謝。

で,お礼?としてどういう意図だったのか,というソースを出してみます。ただし,DBD::mysql の bind_param のバグ(⇒Kazuho@Cybozu Labs: MySQL の高速化プチBKBug #29528 for DBD-mysql: bind_param(..., SQL_FLOAT) ignores exponents)が直ること前提ですけど。

package DBIx::Prepare::Printf;

use strict;
use warnings;
use Carp;

our $VERSION = '0.01';

use DBI qw( :sql_types );

my %type = (
    d => SQL_INTEGER,
    f => SQL_FLOAT,
    s => undef,
);

sub prepare {
    my ($class, $dbh, $sql) = @_;

    my @types;

    $sql =~ s{ \% ( \% | d | f | s ) }{
                $1 eq '%' ? '%' :
                    (push @types, $type{$1}) && '?'
            }exgo;

    my $sth = $dbh->prepare($sql) or croak $dbh->errstr;

    return
        bless {
            sth   => $sth,
            types => \@types,
        }, $class;
}

sub handle { return $_[0]->{sth}; }

sub execute {
    my $self = shift;

    for my $i (0 .. $#_) {
        my $type = $self->{types}->[$i];

        if (defined $type) {
            $self->handle->bind_param(1 + $i, $_[$i], $type);
        }
        else {
            $self->handle->bind_param(1 + $i, $_[$i]);
        }
    }

    return $self->handle->execute();
}

sub AUTOLOAD {
    my $self = shift;
    my $name = our $AUTOLOAD;
    $name =~ s{.*::}{}o;
    return if $name eq 'DESTROY';

    return $self->handle->$name(@_);
}

1;
__END__

=head1 NAME

DBIx::Prepare::Printf

=head1 SYNOPSIS

    use DBI;
    use DBIx::Prepare::Printf;

    my $dbh = DBI->connect('DBI:SQLite:test.db');

    my $sth = DBIx::Prepare::Printf->prepare(
        $dbh,
        'SELECT * FROM t WHERE str = %s OR int = %d OR float = %f'
    );

    $sth->execute('string', 1, 1.1e1);

=cut

何気に oku さんのコードをかりまくりです。あと DBD::SQLite でしかためしてない(型なし!)ので,型まわりがほんとにこれでうまくいくのかわかりません。

んでもこれ,結局 execute() に手間がかかっちゃってパフォーマンス悪そう*2 *3



で,今気づいたんですけど,型を指定してパフォーマンスがあがるとうれしい例って select とかせいぜい update とかであって,prepare() して出来上がった(prepared)statement object に何回も execute() する状況があんまりない気がしてきました。oku さんのアプローチで充分なのか。くやしい!

2007/09/29 追記

execute をなるたけ軽くするというコンセプトなら prepare でコード展開して eval するというこんな感じのほうがいいかも。

sub prepare {
    my ($class, $dbh, $sql) = @_;

    my @types;

    $sql =~ s{ \% ( \% | d | f | s ) }{
                $1 eq '%' ? '%' :
                    (push @types, $type{$1}) && '?'
            }exgo;

    my $sth = $dbh->prepare($sql) or croak $dbh->errstr;

    my $src = 'sub {my $sth = shift;';
    my $i = 0;
    foreach my $type (@types) {
        $src .= sprintf '$sth->bind_param(%u, shift%s);',
                    ++ $i,
                    defined $type ? sprintf(', %d', $type) : q{};
    }
    $src .= '$sth->execute();}';

    my $code = eval $src;

    return
        bless {
            sth   => $sth,
            exec => $code,
        }, $class;
}

sub execute {
    my $self = shift;
    return &{ $self->{exec} }($self->{sth}, @_);
}

execute を複数回実行しないとうまみがでないのは変わりませんが。

*1:SQL::Printf とかそういう namespace でフォーマッタモジュールとするべきではと思った

*2:if (defined $type ) { 〜 のとこがあほっぽいけど仕方ないんですよ

*3:プレースホルダ正規表現で遅めの括弧を使ってるのは %ld とかのための布石