Shibuya.pm #12連動企画
本日開催のShibuya Perl Mongersテクニカルトーク#12のテーマは "No Perl, NoSQL, NoKVS" または "Not only Perl, Not only SQL, Not only KVS" ということなので,今回はそれにあわせてYAPC::Asia 2009でも紹介されていたKiokuDBについて簡単に取り上げてみます。
オブジェクトをまるごと保存する
牧大輔氏も『モダンPerl入門』のなかで,データベースをハッシュテーブルのようにとらえて,「基本的にプライマリキーからデータを持ってくる構成のみにすると,ORMを使用することによりキャッシュの導入も含めてチューニングが楽になります」と書いているように,Perlの世界では最近RDBMSやその上位層で頑張りすぎるより,モデリングの仕方そのものを工夫して実装や保守のしやすさを向上させたりスケーラビリティの確保を目指す現場も増えてきているようです。リレーショナルデータベースのデータをオブジェクトにマッピングするのではなく,関係性まで含んだオブジェクトをまるごと保存したり取り出したりできるようにするKiokuDBもその流れの一環にあるものと見てよいでしょう。
オブジェクトをまるごと保存といってもピンとこないかもしれませんので,まずはテキストも写真も投稿できる掲示板のようなものをつくることを考えてみます。テキストにも写真にも対応したクラスをつくってもよいのですが,今回は写真とテキストについてはクラスをわけてみましょう。
テキストを持たせるクラスは簡単です。
package Post;
use Moose;
has 'name' => (is => 'rw', isa => 'Str');
has 'body' => (is => 'rw', isa => 'Str');
has 'author' => (is => 'rw', isa => 'Person', weak_ref => 1);
__PACKAGE__->meta->make_immutable;
写真を持たせるクラスも,Perlレベルでは大差ありません。が,O/Rマッパを使う場合は当然テキストとは異なる型を持たせることになるでしょうから,ここではクラスをわけてあります。
package Photo;
use Moose;
has 'name' => (is => 'rw', isa => 'Str');
has 'image' => (is => 'rw');
has 'author' => (is => 'rw', isa => 'Person', weak_ref => 1);
__PACKAGE__->meta->make_immutable;
両者をとりまとめるPersonはこんな感じです。postsのなかにはテキスト記事も写真記事も入る予定です。単純にArrayRef型にしておいてもよいのですが,ここではKiokuDBで扱いやすくするためにKiokuDB::Setに対応しているオブジェクトのみ受け付けることにします。
package Person;
use Moose;
use KiokuDB::Util 'set';
has 'name' => (is => 'rw', isa => 'Str');
has 'posts' => (
is => 'rw',
does => 'KiokuDB::Set',
default => sub { set() },
);
__PACKAGE__->meta->make_immutable;
では,実際にオブジェクトの保存と取得をしてみましょう。KiokuDBではいくつかのバックエンドを利用できるようになっていますが,ここでは永続化の手段としてSQLiteを利用します。
use strict;
use warnings;
use KiokuDB;
use KiokuDB::Util 'set';
use Person;
use Post;
use Photo;
my $db = KiokuDB->connect( 'dbi:SQLite:db', create => 1 );
まずは必要なモジュールを読み込んで,保存用のデータベースを作成します。KiokuDBを使う際にはあらかじめスキーマなどを設定する必要はありません。KiokuDBのほうでよきにはからってくれます。
my $person_id;
{
my $scope = $db->new_scope;
my $person = Person->new(name => 'Foo Bar');
my $post = Post->new(
name => 'my post',
body => 'MyPost',
author => $person,
);
my $photo = Photo->new(
name => 'my photo',
image => 'MyPhoto',
author => $person,
);
$person->posts(set($post, $photo));
$person_id = $db->store($person);
}
保存するオブジェクトをつくって,データベースに入れます。保存した$personオブジェクトのUUIDが返ってくるので,あとで参照できるようにスコープ外の変数に保存しておいてください。このIDは,はじめて登録するオブジェクトであれば,自分で指定することもできます。
$person_id = $db->store(my_id => $person);
ただし,既存のIDを持つオブジェクトの場合はstore時にIDを指定するとエラーになるので,実際にはID判定も組み込んでおいたほうが無難です。
if ($db->object_to_id($person)) {
$person_id = $db->store($person);
}
else {
$person_id = $db->store(my_id => $person);
}
いまつくった$person以下のオブジェクトはスコープを抜けたところでメモリから消えます。ここではまだnew_scopeの意味がわからないかもしれませんが,これはひとまずKiokuDBとデータをやりとりする際にはかならず必要なものであると思っておいてください(このスコープはウイークリファレンスの保護などの意味があります)。
{
my $other_scope = $db->new_scope;
my $person = $db->lookup($person_id);
foreach my $post ($person->posts->members) {
print $post->name, " by ", $post->author->name, "\n";
}
}
別のスコープで,今度は先ほど保存したはずのオブジェクトを取り出してみましょう。Personオブジェクトだけでなく,Personオブジェクトのなかに保存されていたPostやPhotoのオブジェクトや,そこからのウイークリファレンスも正しく復元されていることが確認できます(もちろん実際の掲示板をつくるときにはここでpostsのメンバーをそれぞれ適切なテンプレートに埋め込む作業が発生することでしょう)。
KiokuDBはアプリケーションの仕様が変わっても柔軟に対応できます。今度はムービー記事も投稿できるようにしたいという要望があがってきました。RDBMSならテーブルの変更やらなにやらが必要になるところですが,KiokuDBの場合は表現するクラスを追加するだけで済みます。
package Movie;
use Moose;
has 'name' => (is => 'rw', isa => 'Str');
has 'movie' => (is => 'rw');
has 'author' => (is => 'rw', isa => 'Person', weak_ref => 1);
__PACKAGE__->meta->make_immutable;
Movieを追加するコードはこうなります。
{
use Movie;
my $yet_another_scope = $db->new_scope;
my $person = $db->lookup($person_id);
my $movie = Movie->new(
name => 'my movie',
movie => 'MyMovie',
author => $person,
);
$person->posts->insert($movie);
$db->store($person);
}
なお,このような更新部分はまるごとトランザクションでくくってしまうこともできます。この場合はなくても動作しますが,あったほうが安全ですし,一般的には速くなることが多いようです。
$db->txn_do(sub { ... });

