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

第39回DBIx::Skinny:DBIx::Classに不満を感じたら

DBIC以降の選択肢

今回はデータベース話の締めくくりとして、DBIx::Class以降に登場したいくつかの選択肢についてざっくりまとめておきます。これらはいずれも若く、DBICに比べてユーザ数も少ないためドキュメントなどの整備が遅れている部分もありますが、今回とりあげるものの多くは日本人がつくっているものですから、英語圏で開発が進められているものより疑問や要望は送りやすいはず。気になることがあったらぜひそれぞれの作者氏に伝えていただければと思います。

DBIx::Skinny

nekokakこと小林篤氏のDBIx::Skinnyは、今回紹介するもののなかではもっとも実績豊富なものといってよいでしょう。開発の動機については氏のブログによくまとまっていますが、標準で用意されているCRUDメソッドを使うときだけでなく、速度を稼ぐために生のSQLを書いたときでもinflateなどの補助機能を使えるようになっているのがこのモジュールの肝。2009年末にはAdvent Calendarの一環として25回にわたる連載記事が書かれているので、一般的な用途であれば特に使い方に困ることはないと思います。スキーマの設定からデータベースの操作まで含めて最低限のサンプルを書くならこのような感じになるでしょうか。install_inflate_ruleのおかげで、search_by_sqlで生のSQLを書いているにもかかわらずcreated_atがきちんとTime::Pieceのオブジェクトになっているのがポイントです。

use strict;
use warnings;

# マッパを用意
package MyDB;
use DBIx::Skinny;

# スキーマを書いて
package MyDB::Schema;
use DBIx::Skinny::Schema;
use Time::Piece;
install_table user => schema {
  pk 'id';
  columns qw(id name created_at);
};

# inflateのルールはまとめておけます
install_inflate_rule '^.+_at$' => callback {
  inflate { Time::Piece->new(shift) };
  deflate { Time::Piece->new(shift)->epoch };
};

# ここから動作確認
package main;
my $db = MyDB->new({ dsn => 'dbi:SQLite::memory:' });

# データベースハンドルを直接操作
$db->dbh->do('create table user (
  id integer primary key autoincrement,
  name text,
  created_at integer
)');

# 用意されているメソッドを使う例
$db->insert(user => { id => 1, name => 'foo' });

# 生SQLを使った検索。ここでは配列コンテキストで
my ($row) = $db->search_by_sql(
  'select id, name, created_at from user where id = ?', [1]);

# rowオブジェクトの中身を確認
print $row->name, "\n";
print $row->created_at->ymd, "\n";

Teng

DBIx::Skinnyはよくできたモジュールですが、誕生から2年が過ぎて、細々としたところでかゆいところに手が届かなかったり扱いにくかったりする部分もあることがわかってきました。そのため、既存のSkinnyとは別に、先日からはTengという後継プロダクトの開発が始まっています。先週開催されたKamakura.pmのイベントなどでも発表がありましたが、こちらは当初DBIx::Skinという名前で開発されていたことからもわかるようにSkinny以上に薄いO/Rマッパを目指して書かれているのが特徴で、Skinnyでは標準で組み込まれていたいくつかの機能は、廃止されたり、プラグインに追い出されました。また、SQLの構築部分もSQL::Makerという別パッケージにまとめられています。一方で、需要が高く、内部的な変更の影響も受けやすいスキーマのダンプやロードについては自前で用意するようになりました(その他の変更点については同じく小林氏のブログ記事などをご覧ください⁠⁠。まだ開発開始から一ヶ月ほどしかたっていないため細かなAPIは変わる可能性がありますが、基本的なところはすでにできていますし、ソースコードの行数もSkinnyの半分ほどになっているので、全容をつかむのはそうむずかしくないでしょう。先ほどのコードをTengで書き直すと、このような感じになります。この原稿を書いている時点ではinflate_ruleはまだ宙ぶらりんの状態でしたが、Skinnyに慣れた人であればさほど違和感なく移行できるはずです。

use strict;
use warnings;

# マッパを用意
package MyDB;
use parent 'Teng';

# スキーマを書いて
package MyDB::Schema;
use Teng::Schema::Declare;
use Time::Piece;
table {
  name 'user';
  pk 'id';
  columns qw(id name created_at);
  inflate created_at => sub { Time::Piece->new(shift) };
  deflate created_at => sub { Time::Piece->new(shift)->epoch };
};

# この例の場合、動作確認の部分はSkinnyのと同じです
package main;
my $db = MyDB->new({ connect_info => ['dbi:SQLite::memory:']});
$db->dbh->do('create table user (
  id integer primary key autoincrement,
  name text,
  created_at integer
)');

$db->insert(user => { id => 1, name => 'foo' });
my ($row) = $db->search_by_sql(
  'select id, name, created_at from user where id = ?', [1]);
print $row->name, "\n";
print $row->created_at->ymd, "\n";

Data::Model

Yappoこと大沢和宏氏のData::Modelは、Data::という名前空間の下にあることからもわかるように、SkinnyやTengとはやや毛色が異なるのですが、Skinnyとほぼ同時期に開発が始まったこともあってお互いに影響を与えあっています。これも開発の意図については氏のブログによくまとまっていますし、2009年にはSkinnyともどもAdvent Calendarにまとまった記事が書かれているので、ふつうに使う分には特に困ることはないでしょう。ここで用意したサンプルではcolumn_sugarを使って複数のテーブルでカラム定義やinflateの設定を共有しているところと、テーブルの作成に生のSQLを使っていないところがポイントですが、細かいところでは、キャッシュされたデータがあればキャッシュを優先して使う設定もしてあります[1]⁠。

use strict;
use warnings;

package MyDB;
use base 'Data::Model';
use Data::Model::Schema;
use Data::Model::Driver::DBI;
use Data::Model::Driver::Cache::HASH;
use Time::Piece;

# スキーマを定義するときにも使う基本のドライバ
my $driver = Data::Model::Driver::DBI->new(
  dsn => 'dbi:SQLite::memory:',
);

base_driver $driver;

# キャッシュを透過的に扱うためのドライバ
my $cache = Data::Model::Driver::Cache::HASH->new(
  fallback => $driver
);

# 複数のテーブルに共通の部分はまとめられます
column_sugar '_.at' => int => {
  unsigned => 1,
  default  => sub { time },
  inflate  => sub { Time::Piece->new(shift) },
  deflate  => sub { Time::Piece->new(shift)->epoch },
};

column_sugar '_.id' => int => {
  unsigned => 1,
};

# スキーマ定義。別名を使ったり、追加のメソッドをはやしたり
install_model user => schema {
  key 'id';
  column '_.id' => 'id' => { auto_increment => 1 };
  column '_.at' => 'created_at';
  columns qw(name);
  add_method emails => sub {
    my $row = shift;
    map { $_->email } $row->get_model->get(
      email => { index => { user_id => $row->id }}
    );
  };
};

install_model email => schema {
  driver $cache; # このテーブルだけはキャッシュを有効に
  index 'user_id';
  column '_.id' => 'user_id';
  columns qw(email);
};

# ここから動作確認
package main;
my $db = MyDB->new;

# スキーマの定義を実際のデータベースに反映
for my $name ($db->schema_names) {
  my $dbh = $db->get_base_driver($name)->rw_handle;
  $dbh->do($_) for $db->as_sqls($name);
}

# データを追加
my $user = $db->set(user => { name => 'foo' });
$db->set(email => {
  user_id => $user->id,
  email   => 'foo@localhost'
});

# ここでは主キーで検索していますが、SQLを使った検索もできます
$user = $db->lookup(user => $user->id);
print $user->name, "\n";
print $user->emails, "\n";
print $user->created_at->ymd, "\n";

DBIx::ObjectMapper

2010年のYAPC::Asiaでも発表があった大石英介氏のDBIx::ObjectMapperは、いわゆるData Mapperパターンを実装したものです。Data Mapperパターンではアプリケーション側で利用するクラスとデータベースのテーブルをかならずしも1対1で対応させる必要がなく、アプリ側ではアプリ側の最適な実装に、データベース側ではデータベース側の最適な実装に、とわけられるので、変更や拡張が長期的に続いていく(アプリケーションのクラスとデータベースのスキーマが乖離しやすい)大規模アプリケーション向きといわれていますが、もう少し身近なところでは、オブジェクトとデータベースが密接に結びついていないためテストを書きやすくなる(いちいちデータベースにダミーデータを食わせてSQLを実行しなくてもオブジェクトをつくれる)のと、アプリケーション側のオブジェクトにはデータベースの接続情報などが入っていないため、デバッグのときなど気軽にオブジェクトの中身をダンプできるのが長所といえるでしょうか。単純な例ではいわゆるActive Recordパターンよりも記述が冗長になるデメリットが目立ってしまうのですが、このサンプルではメタデータとして格納されているテーブル情報を利用してテストデータを挿入したあと、メタデータに入っているテーブルと、アプリケーション側で使うクラス(MyUser, MyEmail)とをマッピングして、最後にリレーションを含むデータの取得と更新を試しています。詳しい使い方については、YAPC::Asiaでの発表のほか、2010年のAdvent Calendarの記事や、付属のテストが参考になります。

use strict;
use warnings;

# アプリで使うクラスを用意
package MyUser;
use base qw/Class::Accessor::Fast/;
__PACKAGE__->mk_accessors(qw/id name emails/);

# マッピングは外に出せるように別パッケージにまとめてあります
package MyMapper;
use DBIx::ObjectMapper;
use DBIx::ObjectMapper::Engine::DBI;

sub init {
  my $class = shift;
  my $_mapper = DBIx::ObjectMapper->new(
    engine => DBIx::ObjectMapper::Engine::DBI->new({
      dsn => 'dbi:SQLite::memory:',
      on_connect_do => [
        q/create table user (
            id integer primary key autoincrement, name text )/,
        q/create table email (
            id integer primary key autoincrement,
            user_id integer references user(id), email text )/,
      ],
    }),
  );
  $_mapper->metadata->autoload_all_tables;

  # テーブルのメタデータとアプリ用のクラスをマッピング
  $_mapper->maps(
    $_mapper->metadata->table('user') => 'MyUser',
    attributes => {
      properties => {
        emails => {
          isa => $_mapper->relation(has_many => 'MyEmail'),
        },
      },
    },
  );

  # 面倒なときはクラスの定義もおまかせにできます
  $_mapper->maps(
    $_mapper->metadata->table('email') => 'MyEmail',
    constructor => { auto => 1 },
    accessors   => { auto => 1 },
    attributes  => {
      properties => {
        user_id => {
          isa => $_mapper->relation(belongs_to => 'MyUser'),
        },
      },
    },
  );
  $_mapper;
}

# ここから動作確認
package main;
my $mapper = MyMapper->init;

# テーブルのメタデータから直接データを入れることもできます
$mapper->metadata->table('user')->insert->values(
  id   => 1,
  name => 'foo'
)->execute;

$mapper->metadata->table('email')->insert->values(
  user_id => 1,
  email   => 'foo@localhost'
)->execute;

# 本来の使い方はこちら
my $session = $mapper->begin_session;
my $user = $session->get(MyUser => 1);
print $user->name, "\n";
print map { $_->email, "\n" } @{ $user->emails };

# データの更新もセッションのなかで
$user->name('foo');
push @{ $user->emails }, MyEmail->new(email => 'bar@localhost');
$session->commit;

# あらためてセッションを作り直してデータを再取得
$session = $mapper->begin_session;
$user = $session->get(MyUser => 1);
print $user->name, "\n";
print map { $_->email, "\n" } @{ $user->emails };

ORLite

ここまで見てきたものはMySQLであろうとPostgreSQLやSQLiteであろうと基本的にはそれほど意識しなくても使えるようになっていましたが、どうせデータベースの乗り換えなどしないのだからと割り切るのであれば最初から特定のデータベースにすり寄ったO/Rマッパを書いてもよいはずです。アダム・ケネディ(Adam Kennedy)氏のORLiteは、SQLiteに特化したO/Rマッパとして、連載第17回で紹介したPadreや、CPANDBORDB::CPANUploadsORDB::CPANTSORDB::CPANRTなどのCPANまわりのツール群のなかで使われています(これらのツールは必要に応じて各サーバに用意されているデータベースのダウンロードや解凍をしたり、データベースのスキーマから動的にクラスを作成したりもしてくれます⁠⁠。どのテーブルがどのクラスにマッピングされているかなどはいまのところ自分でデータベースハンドルを取りだしてクエリを発行しないとわからないので、事前にある程度概要をつかんでおく必要はありますが、CPANの統計をとったり検索サイトなどをつくるのであればワンライナーでも一覧を取り出せますので、使い方を覚えておくと便利なことがあるかもしれません。

use strict;
use warnings;
use ORDB::CPANUploads;
use Time::Piece;

ORDB::CPANUploads::Uploads->iterate(
  "where type = ? and author = ?",
  "cpan", "ISHIGAKI",
  sub {
    printf "%s: %s\n",
      $_->dist,
      Time::Piece->new($_->released)->ymd,
  }
);

ほかにもいろいろありますが

今回は比較的新しく、かつCPANにリリースされるくらいには成熟しているものという条件でいくつかのモジュールを取り上げてみましたが、もちろんCPANやgithubにはここで取り上げたほかにも新旧あわせてさまざまな選択肢が公開されています。それぞれに特徴(向き不向き)がありますので、ご自分の必要にあわせて適切なものを選んでください。

おすすめ記事

記事・ニュース一覧