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

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

この記事を読むのに必要な時間:およそ 5.5 分

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";
※1

テストなどの際にはこれらのドライバをあとから設定することもできますが,スキーマを動的に生成している場合は適切なSQLを生成できるようにあらかじめダミーのドライバを指定しておく必要があります。

著者プロフィール

石垣憲一(いしがきけんいち)

あるときは翻訳家。あるときはPerlプログラマ。先日『カクテルホントのうんちく話』(柴田書店)を上梓。最新刊は『ガリア戦記』(平凡社ライブラリー)。

URLhttp://d.hatena.ne.jp/charsbar/

コメント

コメントの記入