オブジェクトを key-value ストアに直列化する
透過的に memcached を扱える OR マッパをみた。主な興味は「オブジェクトをどう直列化するか」にある。key はなにで value はなんなのか。
準備
まず、データベースを SQLite で作った。
% sqlite3 a.db SQLite version 3.4.0 Enter ".help" for instructions sqlite> create table books (id integer primary key, title, price); sqlite> ^D%
DBIx::MoCo
DBIx::MoCo ははてなが作って使っている OR マッパ。
DBIx::MoCo クラスにはキャッシュががっちりくっついている。指定しない場合でも DBIx::MoCo::Cache::Dummy で Null Object パターン になっているので if で分岐とか書かずにすむのはうれしい。
まずこんなクラスを作ってみた。
package CacheLogger;
use Cache::Null;
use base qw(Cache::Null);
use Data::Dumper;
sub set {
my ($self, $key, $value) = @_;
print '[set] ', $key, "\n";
print Dumper($value);
shift->SUPER::set(@_);
}
sub get {
my ($self, $key) = @_;
print '[get] ', $key, "\n";
shift->SUPER::get(@_);
}
これを cache_object で指定する。
use DBIx::MoCo;
{
package A::DataBase;
use base qw(DBIx::MoCo::DataBase);
__PACKAGE__->dsn('dbi:SQLite:dbname=a.db');
}
{
package A::Book;
use base qw(DBIx::MoCo);
__PACKAGE__->db_object('A::DataBase');
__PACKAGE__->table('books');
__PACKAGE__->primary_keys(qw(id));
}
A::Book->cache_object(CacheLogger->new);
my $book = A::Book->create(
title => 'Programming Perl',
price => 5565
);
print $book->title, " created.\n";
my @books = A::Book->retrieve(title => 'Programming Perl');
print $books[0]->price, ".\n";
ふつうはもっと User とか Publisher とか定義するはずで、その場合 A::Book のうち db_object を指定する部分を A::MoCo として抽出して Book, User, Publisher に A::MoCo を継承させる。今回は Book しかないのでまとめてしまった。
% perl -I ~/src/DBIx-MoCo-0.18/lib a.pl
[set] A::Book-id-1
$VAR1 = bless( {
'changed_cols' => {},
'object_id' => 'A::Book-id-1',
'id' => 1,
'title' => 'Programming Perl',
'price' => 5565
}, 'A::Book' );
Programming Perl created.
[get] A::Book-title-Programming Perl
[set] A::Book-id-1
$VAR1 = bless( {
'changed_cols' => {},
'object_id' => 'A::Book-id-1',
'id' => '1',
'title' => 'Programming Perl',
'price' => '5565'
}, 'A::Book' );
5565
%
key はクラス名 + カラム名 + そのカラムの値、value はだいぶごちゃごちゃしてる。
DBIx::MoCo::create は Class::Trigger で after_create というトリガーを呼んでいる。ここで store_self_cache が呼ばれてインスタンスがキャッシュされる。データベースの列ではなく $self そのものがキャッシュされるんですね。
store_self_cache は POD 曰く
Stores self instance for all own possible object ids.
じゃあ possible なものを増やしてみよう。
{
package A::Book;
use base qw(DBIx::MoCo);
__PACKAGE__->db_object('A::DataBase');
__PACKAGE__->table('books');
__PACKAGE__->primary_keys(qw(id));
__PACKAGE__->unique_keys(qw(title));
}
(books テーブルをクリアしてから) 再実行。
% perl -I ~/src/DBIx-MoCo-0.18/lib a.pl
[set] A::Book-id-1
$VAR1 = bless( {
'changed_cols' => {},
'object_id' => 'A::Book-id-1',
'id' => 1,
'title' => 'Programming Perl',
'price' => 5565
}, 'A::Book' );
[set] A::Book-title-Programming Perl
$VAR1 = bless( {
'changed_cols' => {},
'object_id' => 'A::Book-id-1',
'id' => 1,
'title' => 'Programming Perl',
'price' => 5565
}, 'A::Book' );
Programming Perl created.
[get] A::Book-title-Programming Perl
[set] A::Book-id-1
$VAR1 = bless( {
'changed_cols' => {},
'object_id' => 'A::Book-id-1',
'id' => '1',
'title' => 'Programming Perl',
'price' => '5565'
}, 'A::Book' );
[set] A::Book-title-Programming Perl
$VAR1 = bless( {
'changed_cols' => {},
'object_id' => 'A::Book-id-1',
'id' => '1',
'title' => 'Programming Perl',
'price' => '5565'
}, 'A::Book' );
5565
%
set が増えていて value は同じ。空間効率より時間効率をとったのか、わりと富豪っぽい。
Data::ObjectDriver
Data::ObjectDriver は Six Apart です。名前が長いので以下 DOD とします。
DOD には Cache::set / Cache::get をつかうものが無く CacheLogger はそのままではうまく使えない。環境変数 DOD_DEBUG でログを出せるので、そっちを使った。
use strict;
use warnings;
{
package A::Book;
use base qw(Data::ObjectDriver::BaseObject);
use Data::ObjectDriver::Driver::DBI;
use Data::ObjectDriver::Driver::Cache::Memcached;
use Cache::Memcached;
my $driver = Data::ObjectDriver::Driver::DBI->new(
dsn => 'dbi:SQLite:dbname=a.db'
);
$driver = Data::ObjectDriver::Driver::Cache::Memcached->new(
cache => Cache::Memcached->new({ servers => ['localhost:11211'] }),
fallback => $driver
);
__PACKAGE__->install_properties({
driver => $driver,
datasource => 'books',
columns => [qw(id title price)],
primary_key => 'id',
});
}
my $book = A::Book->new(
title => 'Programming Perl',
price => 5565
);
$book->save;
print $book->title, " created.\n";
my @books = A::Book->search({ title => 'Programming Perl' });
print $books[0]->price, "\n";
Data::ObjectDriver::Driver::Cache::Memcached が Decorator パターン になっているのがかっこいい。Driver は普通はわけるんだろうけど MoCo と同様に Book にまとめている。
% DOD_DEBUG=1 perl -I ~/src/Data-ObjectDriver-0.05/lib a.pl
$VAR1 = 'INSERT INTO books
(title, price)
VALUES (?, ?)
';
$VAR2 = {
'title' => 'Programming Perl',
'price' => 5565
};
in file a.pl line 32
Programming Perl created.
$VAR1 = 'SELECT books.id
FROM books
WHERE (books.title = ?)
';
$VAR2 = [
'Programming Perl'
];
in file a.pl line 35
$VAR1 = 'MEMCACHED_GET_MULTI ?';
$VAR2 = [
'A::Book:1'
];
in file a.pl line 35
$VAR1 = 'SELECT books.id, books.title, books.price
FROM books
WHERE (books.id IN (?))
';
$VAR2 = [
'1'
];
in file a.pl line 35
$VAR1 = 'MEMCACHED_ADD ?';
$VAR2 = [
'A::Book:1',
{
'columns' => {
'id' => '1',
'title' => 'Programming Perl',
'price' => '5565'
}
}
];
in file a.pl line 35
5565
%
create ではキャッシュは関係なく、select でプライマリキーからその他のカラムを取得するためにキャッシュを使っている。
キャッシュの key にカラム名は無い。Data::ObjectDriver::Driver::BaseCache::cache_key と DBIx::MoCo::object_ids を比べると、そもそも DOD は MoCo の様に複数の key に同じ value を set したりはしなそうだ。一方 MoCo に無くて DOD にあるものとして、キャッシュの「バージョン」を指定する仕組みが挙げられる。
粒度おおきめ
どちらも思ったより粒度が大きかった。例えば value をカラムごとにばらばらに保存すれば、それぞれの expire を細かく制御できて理論上は柔軟性があがるけど、そういうことはしないのかな。確かに、その柔軟性は必要かといわれると自信がない。
スケールするサイトのアーキテクチャ考 で紹介されている High Scalability の A Bunch of Great Strategies for Using Memcached and MySQL Better Together でも MySQL にキャッシュがあるのに memcached を使う理由の一つに
The query cache is row based. Memcached can cache any type of data you want and it isn’t limited to caching database rows. Memcached can cache complex complex objects that are directly usable without a join.
として、さらに「Miscellaneous」では
Don’t think row-level (database) caching, think complex objects.
と主張している。books が publisher_id で publishers にむすびついているなら publishers.name もひいていれておけ、とかそういうことでしょうか。
参考
MoCo, DOD の使い方はそれぞれ Introduction to DBIx::MoCo, Perl OR Mappers を参考にしました。ありがとうございます。