DBIx::Skinnyを使った際のCache方法考察

DBIx::SkinnyにはDODやData::Modelのようにキャッシュを透過的に扱う
便利機能はありません。

無いのでラッパーを書きませう。

毎度の事でデモは
http://github.com/nekokak/p5-dbix-skinny-sample/tree/master/cache/
に置いてあります。

ユーザテーブルがあるとします。

CREATE TABLE user (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    name       VARCHAR(255) NOT NULL,
    UNIQUE(name)
);

ユーザの情報をキャッシュからひけなければDBから引っ張って
キャッシュしておき、次に使う時はキャッシュデータを使うという典型的なパターンです。

userテーブルの定義などはこのようにします。
今回はinflate/deflateも一緒にやってみましょう。
userテーブルのnameカラムがDemo::Nameパッケージからinflateされたりdeflateされたりします。

package Demo::DB;
use DBIx::Skinny;

package Demo::DB::Schema;
use DBIx::Skinny::Schema;
use Demo::Name;

install_table user => schema {
    pk 'id';
    columns qw/id name/;
};

install_inflate_rule '^name$' => callback {
    inflate {
        my $value = shift;
        Demo::Name->new($value);
    };
    deflate {
        my $obj = shift;
        $obj->get_name;
    };
};

1;


inflate/deflateさせるモジュールはなんでもよいです。
DateTimeとかはよくやりますね。
今回は適当にでっちあげました。

package Demo::Name;
use strict;
use warnings;
use utf8;

sub new {
    my ($class, $name) = @_;
    bless {name => $name}, $class;
}

sub get_name { $_[0]->{name} }

1;


キャッシュを扱うモジュール
Cache::Memcached::Fastを適当につかってますがなんでもいいでしょう。
get_callbackメソッドの部分が今回の肝ですね。
コードはサンプルなのでまぁ使い方に合わせて変えてあげるとよいでしょう。

package Demo::Cache;
use strict;
use warnings;
use utf8;
use Cache::Memcached::Fast;

sub new {
    bless {
        memd => Cache::Memcached::Fast->new(
            {
                namespace => __PACKAGE__,
                servers   => ['127.0.0.1:11211'],
            }
        ),
    }, __PACKAGE__;
}

sub get_callback {
    my ($self, $opt) = @_;

    my $data = $self->{memd}->get($opt->{cache_key});
    if (defined $data) {
        return $opt->{on_complete} ? $opt->{on_complete}->($data) : $data;
    }

    $data = $opt->{callback}->();
    if (defined $data) {
        $self->{memd}->set($opt->{cache_key} => $data, $opt->{exptime});
        return $opt->{on_complete} ? $opt->{on_complete}->($data) : $data;
    }

    return;
}

1;


使い方

#! /usr/local/bin/perl
use strict;
use warnings;
use lib qw(./lib);
use Demo::DB;
use Demo::Cache;
use Demo::Name;
use Data::Dumper;

my $db = Demo::DB->new;
$db->connect({dsn => 'dbi:SQLite:dbname=./db/demo_user.db', });

my $name = Demo::Name->new('nekokak');
$db->insert('user', { name => $name });

my $cache = Demo::Cache->new;
my $row = $cache->get_callback(
    {
        cache_key => 'user',
        exptime   => 30,
        callback  => sub {
            my $user = $db->single('user', { name => 'nekokak' });
            $user ? $user->get_columns : undef;
        },
        on_complete => sub {
            my $user = shift;
            $db->data2itr('user', [$user])->first;
        },
    }
);

warn Dumper $row;
warn Dumper $row->name;

$db->delete('user');


Demo::Cacheモジュールのget_callbackメソッドにいろいろパラメータをわたしています。
cache_key/exptimeはいいとして、
callbackに渡しているコードリファレンスはキャッシュからデータが取れなかったときに呼び出されるコールバックになります。
on_completeはデータが取得できた時に呼び出されるコールバックになります。
コールバック内で

$db->data2itr('user', [$user])->first;

ということをやってますが、DBIx::Skinnyには配列のデータ構造をDBIx::Skinnyイテレータで扱う機能があります。
イテレーターから呼び出したrowオブジェクトはDBからデータを取得したのと同じ構造のRowオブジェクトにしてくれるので
inflateの処理もしてくれてます。
勿論1レコードだけじゃなくて有る程度固まったデータ構造を取得する処理を
callback内で書いて、arrayrefをキャッシュしておき、on_completeでまるっとイテレータに戻すことも可能です。

data2itrというメソッド名を昔突っ込まれたことあるんですけどいい名前が思い浮かばなかったので募集中。

こういうラッパーをかけば好きに簡単にキャッシュをあつかえますよというお話でした。