Mixin 機能その2 mixin method に柔軟なクエリをB!

はじめに

昨日は Data::Model::SQL を使って様々な SQL を作り出す事を紹介しました。

さていよいよ5日目に作った MyBookmark::Mixin::Count に条件で絞り込んだレコードの count を取る機能をつけます。

count メソッドの要件をまとめる

実際にコードを作る前に count メソッドをどう使うか考えましょう。

今回は次のようあ使い方を想定します。

# 検索条件無し
$obj->count( $table_nae );

# プライマリキーで絞り込み
$obj->count( $table_nae => 1 );

# 複合プライマリキーで絞り込み
$obj->count( $table_nae => [1, 2] );

# index で絞り込み
$obj->count( $table_nae => { index => { idx_name => 1 } } );

# その他 where とかもでも絞り込み
$obj->count( $table_nae => { where => [ column => 'x' ] } );

# プライマリキー or 複合プライマリキーと where とかで絞り込み
$obj->count( $table_nae => 1 => { where => [ column => 'x' ] } );

プライマリキーで絞り込んだら数値は1以下になるんですが、複合プライマリキーの時にキーをひとつだけ入れた時とかに使えるので入れておきます。

count メソッドを拡張する

さて、実際に count メソッドをしてみましょう。元のコードは5日目に作った Count.pm です。

まずは、引数を受け取ってテーブルに対応したスキーマ定義オブジェクトを取り出します。

sub count {
    my($self, $model, $key, $query) = @_;
    my $driver = $self->get_driver($model);
    my $schema = $self->get_schema($model);

get_driver メソッドで、ストレージドライバーを取得します。

get_schema メソッドは、未定義のテーブル名を入れると自動的にエラーを発生するためパラメータチェックは不要です。

次は、検索条件が特定の ARRAY ref/HASH ref/string になってるか調べます。

    if (defined $key && ref($key) eq '') {
        # primary key は array-ref である必要がある
        $key = [ $key ];
    } elsif (ref($key) ne 'ARRAY') {
        $query = $key;
        undef $key;
    }
    unless (defined $query) {
        $query = {};
    } else {
        die "query should be hash-ref" unless ref($query) eq 'HASH';
    }

第二引数が省略可能なのでちょっと複雑になってます。

こんどは SQL オブジェクト構築です。

昨日紹介したメソッドを使って index やら primary key やらの設定も行います。

$driver は Data::Model::Driver::DBI のインスタンスのため Data::Model::Driver::DBI からクラスメソッドを呼ばなくても良くなっています。

    my %q = (
        select => 'COUNT(*) AS count',
        from   => $model,
        %{ $query }
    );
    my $index = $q{index};
    my $sql_obj = Data::Model::SQL->new( %q );
    $driver->add_key_to_where($sql_obj, $schema->key, $key) if $key;
    $driver->add_index_to_where($schema, $sql_obj, $index) if $index;
    my $sql = $sql_obj->as_sql;

select で取ってくるカラムに COUNT(*) AS count を指定してレコードの件数を取れるようにします。

bind と bind_column メソッドで、検索条件のカラム名とバインドされるべき値が取れると昨日説明しましたが、これを DBI のステートメントハンドルに bind する為の Data::Model::Driver::DBI の便利メソッドに渡す前処理をします。

    # パラメータをバインドする為の前処理
    my @params;
    for my $i (1..scalar(@{ $sql_obj->bind })) {
        push @params, [ $sql_obj->bind_column->[$i - 1], $sql_obj->bind->[$i - 1] ];
    }

前処理が終わったら実際にクエリを発行します。

    my $sth;
    my $count = 0;
    eval {
        # SELECT する為の dbh を取得
        my $dbh = $driver->r_handle;
        $driver->start_query($sql, $sql_obj->bind); # クエリログ用
        $sth = $dbh->prepare($sql);
        # パラメータを bind する
        $driver->bind_params($schema, \@params, $sth);
        $sth->execute;
        $sth->bind_columns(undef, \$count);
        $count = 0 unless $sth->fetch;
        $sth->finish;
        $driver->end_query($sth); # クエリログ用
    };
    if ($@) {
        # エラーがあったらスタックトレース吐いて終了
        $driver->_stack_trace($sth, $sql, $sql_obj->bind, $@);
    }
    return $count;

$driver に生えている bind_params メソッドで、先程作った @params を元にステートメントハンドルに検索条件の値を bind していきます。

それぞれ start_query と end_query は、今度紹介するクエリトレース機能でトレースするために使う為のメソッドです。

最後の _stack_trace は、クエリ実行中に何かしらのエラーがあったときにスタックトレースを出して死ぬメソッドです。なんでか private method でやんの。。。

使ってみよう

では、早速使ってみよう

    $bookmark->set(
        user => 1 => { nickname => 'Yappo' }
    );
    $bookmark->set(
        user => 11 => { nickname => 'nekokak' }
    );
    $bookmark->set(
        user => 101 => { nickname => 'kan' }
    );

としてレコードを作って

    my $count = $bookmark->count(
        user => {
            where => [
                id => { '<' => 100 }
            ],
        }
    );
    print "Count: $count\n"; # Count: 2

として使えます。

primary key で絞り込みなどだと

    $bookmark->set( bookmark => [1, 1] );
    $bookmark->set( bookmark => [2, 1] );
    $bookmark->set( bookmark => [3, 1] );
    $bookmark->set( bookmark => [4, 1] );
    $bookmark->set( bookmark => [1, 2] );
    $bookmark->set( bookmark => [2, 2] );
    $bookmark->set( bookmark => [3, 2] );
    $bookmark->set( bookmark => [4, 2] );

として、レコードを作って以下のレコードで絞り込みます。

    # url_id = 1 のレコード数
    warn $bookmark->count( bookmark => 1 );
    # url_id = 2 で user_id < 2 のレコード数
    warn $bookmark->count( bookmark => 1 => { where => [ user_id => { '<' => 2 } ] } );
    # user_id = 2 のレコード数
    warn $bookmark->count( bookmark => { index => { user_id => 2 } } );

それぞれ、 2, 1, 4 と結果が帰ってくれば成功です。

まとめ

二日がかりで Count を取る為の Mixin を作ってみました。 Data::Model のコアにはまだ無い mixin なので、もうすこしブラッシュアップしてから取り込もうかなと思います。

なお、これを書くに当たりもろもろの API の使い勝手悪くて嫌になったので API が変更されるかもです。ので、変更されたら追記しようと思います。