DBI をバックエンドにした CouchDB みたいなの

CouchDB は面白いのですが、Erlang で書かれた daemon を動かす必要があるので、環境によっては使いにくい。
ということで、Perl + DBI のみで似たような動作をするものを作ってみました。daemon じゃなくて、Perl から直接 DB を扱うライブラリです。
Coderepos に置きました。http://svn.coderepos.org/share/lang/perl/DBIx-CouchLike/trunk

できることは単純。

  • (key => 任意の構造の hash ref) の CRUD
  • CouchDB ぽい map / reduce

対応している DBD はとりあえず SQLitePostgreSQL で。

CRUD

use DBIx::CouchLike;
use DBI;
$dbh   = DBI->connect("dbi:SQLite:dbname=test.db");
$couch = DBIx::CouchLike->new({ table => "foo", dbh => $dbh });
$couch->create_table(); # はじめて使う場合テーブル作成

# CREATE (id 自動発行)
$id = $couch->post({ name => 'animal', tags => ['dog', 'cat']});

# CREATE (id を指定)
$couch->post( $id => { name => 'animal', tags => ['dog', 'cat']} );
# または
$couch->post({ _id => $id, name => 'animal', tags => ['dog', 'cat']});

# RETRIEVE
$obj = $couch->get($id);

# UPDATE
$couch->put( $id, $obj );
# または
$couch->put( $obj ); # $obj->{_id} が存在していること

# DELETE
$couch->delete($id);

これは単に、渡されたオブジェクトを JSON で encode して DBI で扱うだけ。

Map / Reduce

map, reduce のための perl コードを文字列で用意します。
map の第1引数は保存されているオブジェクトそのもの。第2引数は新しい key => value ペアを登録するためのサブルーチンリファレンスです。

$map_sub_str = <<'END_OF_CODE';
sub {
    my ($obj, $emit) = @_;
    for my $tag ( @{ $obj->{tags} } ) {
        # tags の配列を順番に取得して
        # tag => name のペアに変換
        $emit->( $tag => $obj->{name} );
    }
}
END_OF_CODE

reduce の引数は key と value の配列リファレンス。

$reduce_sub_str = <<'END_OF_CODE';
sub {
    my ($keys, $values) = @_;
    # value の数を数えるだけ
    return scalar @$values;
}
END_OF_CODE

用意したコードを _design/*** という id で登録。

$couch->post({
    _id      => "_design/find",
    views    => {
        tags => { map => $map_sub_str },  # map するだけのもの
        tags_count => {                   # map して reduce するもの
            map    => $map_sub_str,
            reduce => $reduce_sub_str,
        },
    },
});

map / reduce で処理するためのオブジェクトを保存。この例は SBM みたいにタグ付けされたデータだと思ってください。

$id_a = $couch->post({ name => "animal", tags => ["dog", "cat"] });
$id_c = $couch->post({ name => "unix",   tags => ["cat", "less", "more"] });

登録された _design/find を取得するには view() を使います。まず map するだけのを取得してみると

@result = $couch->view("find/tags")->all;

以下のような値が返ります。順番は key, value の辞書順 (SQL の ORDER BY) です。

( { key => "cat",  value => "animal", id => $id_a },
  { key => "cat",  value => "unix",   id => $id_c },
  { key => "dog",  value => "animal", id => $id_a },
  { key => "less", value => "unix",   id => $id_c },
  { key => "more", value => "unix",   id => $id_c } )

指定した key だけ取得。

@result = $couch->view("find/tags", { key => "cat" })->all;
( { key => "cat",  value => "animal", id => $id_a },
  { key => "cat",  value => "unix",   id => $id_c } )

map された key => value ペアに対応する document も一緒に取得するには include_docs オプション。

$couch->view("find/tags", { key => "dog", include_docs => 1 })->next;
{ id       => $id_a,
  key      => 'dog'
  value    => 'animal',
  document => {
    _id  => $id_a,
    name => 'animal',
    tags => ['dog', 'cat' ]
  },
}

reduce するほうを取得するとこうなります。

@result = $couch->view("find/tags_count")->all;
({ key => 'cat',  value => 2 },
 { key => 'dog',  value => 1 },
 { key => 'less', value => 1 },
 { key => 'more', value => 1 })
注意とか
  • 分散処理とかレプリケーションとか、そういうのはまったく考慮されていません
  • map, reduce で与えた文字列は eval で評価されますので、くれぐれもご注意ください