モダンPerlの世界へようこそ

第21回KiokuDB:マッピングが複雑すぎると感じたら

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 { ... });

オブジェクトの検索

KiokuDBを使うとオブジェクトの保存や取り出しは非常に簡単になりますが、ため込んだデータも必要に応じて適切に取り出せなければ意味がありません。

検索を有効にしたい場合は、データベースを作成する際に検索用のカラムを指定しておきます。

my $db = KiokuDB->connect(
  'dbi:SQLite:db',
  create  => 1,
  columns => [
    name => { data_type => 'varchar', is_nullable => 1 },
  ],
);

このカラム指定のやり方はDBIx::Classのものと同じです。is_nullableはすべてのデータがnameアトリビュートを持っていれば不要ですが、内部的に生成されるデータの場合特定のアトリビュートを持たないことがあるので、たいていの場合は有効にしておく必要があるでしょう。

検索用のカラムを用意したら、あとはそのカラムを対象に検索をかけるだけです(この部分は将来的にはDBIx::Class互換のAPIになる見込みです⁠⁠。

{
  my $stream = $db->search({ name => { 'like' => '%movie' }});
  while (my $block = $stream->next) {
    foreach my $object (@$block) {
      print $object->name, "\n" ;
    }
  }
}

実際にはどのように保存されているのか

こうして永続化したオブジェクトは、実際にはどのように保存されているのでしょうか。sqlite3やDBI::Shellに付属のdbishコマンドなどを利用すると、生データを確認できます。ここではそれぞれのオブジェクトがばらばらに(独自のIDを持って)保存されていることのみ覚えておけばよいでしょう。

@dbi:SQLite:db> select * from entries;
id,data,class,root,tied,name
'B3250019-4096-1018-85CF-79025712037B','{"__CLASS__":"KiokuDB::Set::Stored","data":["B3848A9D-4096-1018-85CF-79025712037B","D5DEA4B1-4096-1018-85CF-79025712037B","B34F3D07-4096-1018-85CF-79025712037B"],"id":"B3250019-4096-1018-85CF-79025712037B"}','KiokuDB::Set::Stored',0,undef,undef
'B34F3D07-4096-1018-85CF-79025712037B','{"__CLASS__":"Post","data":{"author":{"$ref":"B3005845-4096-1018-85CF-79025712037B.data"},"body":"MyPost","name":"my post"},"id":"B34F3D07-4096-1018-85CF-79025712037B"}','Post',0,undef,'my post'
'B3005845-4096-1018-85CF-79025712037B','{"__CLASS__":"Person","data":{"name":"Foo Bar","posts":{"$ref":"B3250019-4096-1018-85CF-79025712037B.data"}},"id":"B3005845-4096-1018-85CF-79025712037B","root":true}','Person',1,undef,'Foo Bar'
'B3848A9D-4096-1018-85CF-79025712037B','{"__CLASS__":"Photo","data":{"author":{"$ref":"B3005845-4096-1018-85CF-79025712037B.data"},"image":"MyPhoto","name":"my photo"},"id":"B3848A9D-4096-1018-85CF-79025712037B"}','Photo',0,undef,'my photo'
'D5DEA4B1-4096-1018-85CF-79025712037B','{"__CLASS__":"Movie","data":{"author":{"$ref":"B3005845-4096-1018-85CF-79025712037B.data"},"movie":"MyMovie","name":"my movie"},"id":"D5DEA4B1-4096-1018-85CF-79025712037B"}','Movie',0,undef,'my movie'
[5 rows of 6 fields returned]

また、KiokuDB::Cmdをインストールするとついてくるkiokuコマンドを利用するとYAML形式のダンプを見ることもできます。それぞれのオブジェクトがKiokuDB::Entryでラップされている様子やリファレンスがKiokuDB::Referenceであらわされている様子などがうかがえます。

$ kioku dump --dsn dbi:SQLite:db

--- !!perl/hash:KiokuDB::Entry
class: KiokuDB::Set::Stored
data:
- B3848A9D-4096-1018-85CF-79025712037B
- D5DEA4B1-4096-1018-85CF-79025712037B
- B34F3D07-4096-1018-85CF-79025712037B
id: B3250019-4096-1018-85CF-79025712037B
root: 0
--- !!perl/hash:KiokuDB::Entry
class: Post
data:
  author: !!perl/hash:KiokuDB::Reference
    id: B3005845-4096-1018-85CF-79025712037B
  body: MyPost
  name: my post
id: B34F3D07-4096-1018-85CF-79025712037B
root: 0
--- !!perl/hash:KiokuDB::Entry
class: Person
data:
  name: Foo Bar
  posts: !!perl/hash:KiokuDB::Reference
    id: B3250019-4096-1018-85CF-79025712037B
id: B3005845-4096-1018-85CF-79025712037B
root: 1
(後略)

そのままではうまく保存できない場合

KiokuDBはほとんどのオブジェクトをそのまま保存できますが、magic変数やoverloadなどの黒魔法を利用しているオブジェクトについては、そのままではうまくシリアライズできずにエラーを起こすこともあります。このような場合は個別にTypeMapを設定する必要があります。詳しくはKiokuDB::Tutorialなどをご覧ください(このチュートリアルについてはYAPC::Asiaのときにスライドの翻訳を担当してくださった加藤敦氏による日本語訳もKiokuDB::Tutorial::JAとして同梱されています⁠⁠。

O/Rマッパを置き換えるものではなく

KiokuDBはうまく使えば便利なツールですが、既存のO/Rマッパを完全に置き換えてしまうようなものではありません。お互いに得意な分野があるので、用途にあわせて適宜使い分けていくのが吉でしょう。

Catalystを使っている場合はKiokuDBをモデルとして使うためのコンポーネントが用意されているほか、より汎用的な拡張モジュールがKiokuX::* に用意されています。

KiokuDBは2008年5月に始まった若いプロジェクトですから(詳しい誕生の経緯については作者ユーヴァル・コグマン氏のブログにまとめ記事が載っているのでそちらをご覧ください)ユーザ数はまだそれほど多くはないようですが、KiokuDBやMooseの開発チームが所属しているインフィニティ・インタラクティヴ(Infinity Interactive)社をはじめ、すでに数件の導入事例があるようです。

おすすめ記事

記事・ニュース一覧