ドライバを作るB!

はじめに

さぁ、何日か立て続けに Data::Model に用意されている各種 Driver の使い方を紹介してきました。

Driver 編最終回は Driver を作る方法について紹介しましょう。

Driver を作るために必要な知識

Driver を作るためにはある程度規則に従う必要があります。

ゼロから独自の Driver を作るには Data::Model::Driver の中で空定義してあるメソッドを上書きする必要があります。

そして Data::Model::Driver の各メソッドは Data::Model より delegation されているので、 Data::Model のコードも読んでおく必要があります。

このあたりはドキュメントされていません。

また、内部 API が変わってしまうと互換性が無くなる可能性もあり厄介です。

ということで本日は、需要がありそうで簡単そうな Driver を作る方法を軽く紹介します。

Driver::DBI を拡張する

主に一昨日紹介した Driver::DBI::MasterSlave の挙動が気にくわないと言った事で作りたい欲求が出るでしょう。

Driver::DBI::MasterSlave は Driver::DBI の dbh のやりくりを制御する部分を独自にハンドリングして master, slave の dbh を切り替えるという要求にしたがって作られています。

rw_handle, r_handle が返す値をうまい具合にやりくりすれば、独自のレプリケーション対応の Driver がかけます。

Driver::DBI::MasterSlave のコードを元にして、どのあたりをいじれば良いのかを紹介しましょう。

継承するクラス

なので、しちめんどい set,get,update,delte,lookup などの処理は再実装しないで Driver::DBI にやってもらえばいいので、これを継承します。

package Data::Model::Driver::DBI::MasterSlave;
use strict;
use warnings;
use base 'Data::Model::Driver::DBI';

クラス初期化

Data::Model::Driver の初期化は new メソッドの引数を全て bless { %args }, $class のようにして保存してから、 init メソッドを単純に呼び出しています。

なので、独自 Driver の初期化処理は init メソッドの中で行います。

sub init {
    my $self = shift;
    my $master = $self->{master}
        or Carp::croak "'master' configuration is required";
    my $slave  = $self->{slave} || $master;

    if (my($type) = $master->{dsn} =~ /^dbi:(\w*)/i) {
        $self->{dbd} = Data::Model::Driver::DBI::DBD->new($type);
    }
    $self->{dbi_config} = +{
        master => +{ %{ $master } },
        slave  => +{ %{ $slave } },
    };
}

今回は rw_handle, r_handle を自由に差し替えたいという要求を設定しました。

Driver::DBI のでは、このあたりも自由に差し替えするコードを書きやすくしてあります。

状況に応じた DBI のインスタンスを複数作れる用になっており、複数のインスタンスを作るためには DBI の設定を複数設定しておく準備が必要です。

具体的には $self->{dbi_config} に config_name => $config という形で HASH リファレンスを指定します。

Driver::DBI::MasterSlave では master と slave という設定名を使って、二つの設定を保存しています。

$self->{dbd} に Data::Model::Driver::DBI::DBD のインスタンスを入れているところは Driver::DBI で各種 DBD に対応した SQL を吐くために必須ですので注意してください。

dbh を使い分ける

さぁ DBI インスタンスの設定を複数作ったら、あとはそれぞれ使うだけです。

これはrw_handle と r_handle のメソッドをそれぞれ上書きします。

sub rw_handle { shift->_get_dbh('master', @_) };
# トランザクション中は master のみを返す
sub r_handle  { my $self = shift;$self->_get_dbh( ($self->{active_transaction} ? 'master' : 'slave'), @_ ) };

見れば分かりますが $self->_get_dbh(config_name) といった形で _get_dbh のプライベートメソッドを読んでいます。

通常は、このように $self->{dbi_config} に格納した設定名を引数にして呼び出せば、その設定を引数にして自動的に DBI インスタンスを作ってくれるので、それを戻すだけでやりたい事が出来ます。

r_handle では $self->{active_transaction} が真だったら master を見るようにしていますが、これは txn_scope 下では常に master を見るべきという設計によるものです。

今の Data::Model の Driver::DBI では、このあたりもハンドリングしてあげる必要があります。

応用

例えば複数台の slave の設定を設定してランダムにその slave を使いたい場合は下記の用な Driver を書きます。

package Data::Model::Driver::DBI::ManySlave;
use strict;
use warnings;
use base 'Data::Model::Driver::DBI::MasterSlave';

sub init {
    my $self = shift;
    my $master = $self->{master}
        or Carp::croak "'master' configuration is required";
    my $slave  = $self->{slave} ? ref($self->{slave}) eq 'ARRAY'
        ? $self->{slave} : [ $self->{slave} ] : [ $master ];
    $self->SUPER::init(
        master => $master,
        slave  => $slave,
    );
}

slave のオプションを ARRAY ref にする感じです。

殆ど Driver::DBI::MasterSlave の実装を使いまわすので、ここは Data::Model::Driver::DBI::MasterSlave をそのまま継承します。

次は rw_handle と r_handle かと思いますが、 Driver::DBI::MasterSlave の物をそのまま使います。

では、 slave の r_handle を複数から選択するのは選択するの?という疑問ですが、新しく紹介するメソッドを上書きして使います。

dbi_config というメソッドを上書きします。

$self->{dbi_config} に DBI への設定を入れていたと思いますが、この設定を取り出すためのメソッドとして定義されています。

sub dbi_config {
    my($self, $name) = @_;
    return $self->{dbi_config}->{master} if $name eq 'master';
    my $slave = $self->{dbi_config}->{slave};
    return $slave->[rand(@{ $slave })];
}

このようにして master の時は $self->{dbi_config}->{master} を返して、 slave の時は slave の設定をどれかランダムで返すのです。

DBI のインスタンスを作る為のメソッドの中では dbi_config を使って DBI の設定を取得しているので、ここだけを変更すればうまく行きます。

使ってみる

さて、この作った Driver::DBI::ManySlave を使ってみますか。

my $many = Data::Model::Driver::DBI::ManySlave->new(
    master => {
        dsn => 'dbi:mysql:host=master.server:database=test',
    },
    slave => [
        { dsn => 'dbi:mysql:host=slave1.server:database=test' },
        { dsn => 'dbi:mysql:host=slave2.server:database=test' },
        { dsn => 'dbi:mysql:host=slave3.server:database=test' },
        { dsn => 'dbi:mysql:host=slave4.server:database=test' },
    ],
);

これだけです。

とりとめのない話

これだけの為にわざわざコード書くのはちょっと面倒なので Driver::DBI の設定だけでうまく行くようにしようと思います。

現状でも微妙に出来そうなコード片が入ってるのですが、完璧じゃないのでもすこし書き直してから公開しようとおもいます。

Driver::Cache を拡張する

さて、次は透過的なキャッシュをする Driver を独自の物に書いてみましょう。

標準では Perl 固有の HASH の中にキャッシュするか、 Memcached なオブジェクトへのキャッシュしか選択出来ません。

しかし Driver::DBI と比べてもさらにシンプルなんです。

基本的な Driver としての実装は Data::Model::Driver::Cache の中で実装されており、これを継承して Driver::Cache 用のインターフェイスを満たせば OK なんです。

これも既存の Driver::Cache::HASH のコードを元に説明しましょう。

データ追加

データの追加するメソッドを定義します。

sub add_to_cache {
    my($self, $key, $data) = @_;

    my $ret = $CACHE{$key} = $data;
    return if !defined $ret;
    return $ret;
}

add_to_cache の第一引数に key を、第二引数に value が渡されます。

成功したら value をそのまま返してください。

失敗時は undef を返します。

データ取得

データを取得する処理で使われます

sub get_from_cache {
    my($self, $key) = @_;

    my $ret = $CACHE{$key};
    return if !defined $ret;
    return $ret;
}

get_to_cache の第一引数に key が渡されます。

成功したら key に対応する value をそのまま返してください。

失敗時は undef を返します。

データ削除

データを削除する処理で使われます

sub remove_from_cache {
    my($self, $key) = @_;
    
    my $ret = delete $CACHE{$key};
    return if !defined $ret;
    return $ret;
}

remove_from_cache の第一引数に key が渡されます。

失敗したら undef を返してください。

成功したら undef 以外を返してください。

今現在 0 や '' などを返しても失敗したと誤認識するバグが発見されました。

その他

update 処理は、該当する key の削除のみを行うという挙動になっています。

トランザクションとの組み合わせは、現在完全な透過処理が行われません。

lookup_multi 系のクエリは get_multi_from_cache を上書きします。

以下に Memcached で利用してるコードを張り付けます。

sub get_multi_from_cache {
    my($self, $keys) = @_;

    my $ret = $self->{memcached}->get_multi($keys);
    return if !defined $ret;
    return $ret;
}

まったく新規に Driver を作る

もうちょっと詳細に書く予定でしたが、基本的に本日紹介した方法を見れば大体の Driver 作成の要求を満たせるかなと思ったので今回は省略させてください。

もし、そのような需要がある場合は Yappo を捕まえて相談してみてください。

他の DBD 対応

Driver とは直接関係ないですが mysql や SQLite 以外の DBD 対応へのポインタを示します。

Data::Model::Driver::DBI::DBD 以下の名前空間の実装を見てください。

基本的に SQL generator からの delegation されるコードですので delegation 元の Data::Model::Schema::SQL などを読んでみてください。

DBD::Pg に関しては sfujiwara さんが実装してくださったので僕の merge まちです><

まとめ

本日は Driver hack についてあれこれ書きました。

さぁ、明日はいよいよ最終回です。