この投稿は Perl 5 Advent Calendar 2015 の 1日目の記事です。
PerlのORM事情
PerlのメジャーなO/R Mapperの実装として、主に以下の2つがあります。
- DBIx::Class
- Teng
DBIx::Class
DBIx::Class(DBIC)は、ざっくり言うと重厚だけど至れり尽くせりで便利って感じのやつです。
SYNOPSISから引用すると、以下のようにテーブルを定義できます。
package MyApp::Schema::Result::CD;
use base qw/DBIx::Class::Core/;
__PACKAGE__->load_components(qw/InflateColumn::DateTime/);
__PACKAGE__->table('cd');
__PACKAGE__->add_columns(qw/ cdid artistid title year /);
__PACKAGE__->set_primary_key('cdid');
__PACKAGE__->belongs_to(artist => 'MyApp::Schema::Result::Artist', 'artistid');
1;
テーブルごとに使うプラグインを決められたり、 belongs_to
でリレーションシップを定義できたりします。
my @cds = $schema->resultset('CD')->all;
こんな感じで全件取ってきたりできますし、
my $millennium_cds_rs = $schema->resultset('CD')->search(
{ year => 2000 },
{ prefetch => 'artist' }
);
こんな具合でリレーションシップの情報をもとにJOINかけて関連するデータを一気に取ってくることができたりもします。
Teng
TengはDBICと比較すると非常に軽量なORMです。
以下のようにDSLでテーブルを定義できます。
package MyApp::Model::Schema;
use Teng::Schema::Declare;
table {
name 'user';
pk qw/id/;
columns qw/id name/;
};
1;
そしてこんな具合で検索できます。
my $row = $teng->single(user => { id => 1 });
$row->update({ name => 'papix' });
ORMレベルでのリレーションシップのサポートはありません。
もちろん、行オブジェクトのクラスを拡張することは可能であるため、
以下のように行に関連するテーブルの情報を取得するメソッド実装することは可能です。
実際、これで十分である場合も多いでしょう。
package MyApp::Model::Row::User;
sub modules {
my $self = shift;
return $self->handle->search(module => { user_id => $self->id });
}
比較
DBIx::Classは非常に便利な機能を提供しますが、コードは非常に複雑です。
特に、パフォーマンスが非常に重要な課題である場合にDBIx::Classの中の処理を追うことは困難といえるでしょう。
対して、Tengは非常にシンプルでコードも見通しが良く、ある程度までの機能はPluginというかたちで追加することができます。
しかし、リレーションシップについてはテーブル定義のメタオブジェクトで管理していないため、取得することができませんでした。
ある程度以上の規模のデータベースを扱うプログラムになると、テーブルの数が軽く100テーブルを超えます。そのような状況下でリレーションシップの『実装』を手でメンテナンスしていくことは非常に困難です。
自分はある時期に、Tengをベースにリレーションシップのサポートを自前で実装していましたが、やはりコアをいじらないで無理やり実装すると非常に見通しの悪いコードになってしまいます。
そこで、Tengと同じ程度に軽量なリレーションシップをサポートするORMを新しく作りました。それがAnikiです。
Anikiの特徴
AnikiはTengをベースに考えられたリレーションシップをサポートするシンプルなORMです。
Anikiはその機能の多くを他のモジュールに移譲することでシンプルさを実現しています。
たとえば、以下の機能を他のモジュールに移譲しています。
- データベースとの接続のハンドリング:
DBIx::Handler
- トランザクション管理:
DBIx::TransactionManager
- クエリビルダ:
SQL::Maker
- データベースのスキーマのメタオブジェクト:
SQL::Translator::Schema
- スキーマ定義:
DBIx::Schema::DSL
Anikiはそれらを糊のようにスムースにつなげて、その上にリレーションシップやオブジェクトマッピングなどをサポートしています。
たよりがいがありますね!
Anikiの使い方
Aniki にドキュメントがあります。
以下のようにスキーマを定義しましょう。Anikiでは外部キー制約をそのままリレーションシップの定義として利用することが可能です。
use 5.014002;
package MyProj::DB::Schema {
use DBIx::Schema::DSL;
create_table 'module' => columns {
integer 'id', primary_key, auto_increment;
varchar 'name';
integer 'author_id';
add_index 'author_id_idx' => ['author_id'];
belongs_to 'author';
};
create_table 'author' => columns {
integer 'id', primary_key, auto_increment;
varchar 'name', unique;
};
};
特定のカラムのデータを特定のオブジェクトにinflate/deflateしたい場合もあるでしょう。そのような場合はFilterが利用できます。
FilterはなくてもOKです。
package MyProj::DB::Filter {
use Aniki::Filter::Declare;
use Scalar::Util qw/blessed/;
use Time::Moment;
# define inflate/deflate filters in table context.
table author => sub {
inflate name => sub {
my $name = shift;
return uc $name;
};
deflate name => sub {
my $name = shift;
return lc $name;
};
};
inflate qr/_at$/ => sub {
my $datetime = shift;
$datetime =~ tr/ /T/;
$datetime .= 'Z';
return Time::Moment->from_string($datetime);
};
deflate qr/_at$/ => sub {
my $datetime = shift;
return $datetime->at_utc->strftime('%F %T') if blessed $datetime and $datetime->isa('Time::Moment');
return $datetime;
};
};
Anikiを継承して、schemaとfilterを定義したクラスをもとにAnikiをセットアップしましょう。
これで準備は完了です。
package MyProj::DB {
use Mouse v2.4.5;
extends qw/Aniki/;
__PACKAGE__->setup(
schema => 'MyProj::DB::Schema',
filter => 'MyProj::DB::Filter',
);
};
こんな感じでDBオブジェクトをつくりましょう。
my $db = MyProj::DB->new(connect_info => ["dbi:SQLite:dbname=:memory:", "", ""]);
DBIx::Schema::DSLを使っているのでDDLを生成するのも簡単です。
$db->execute($_) for split /;/, MyProj::DB::Schema->output;
AnikiではinsertでINSERT以外の暗黙的なクエリを発行することはありません。
主目的以外の副作用がある場合はメソッド名でそれが明示されます。
my $author_id = $db->insert_and_fetch_id(author => { name => 'songmu' });
$db->insert(module => {
name => 'DBIx::Schema::DSL',
author_id => $author_id,
});
$db->insert(module => {
name => 'Riji',
author_id => $author_id,
});
Tengと違い、search/signleの違いを意識する必要はありません。
SELECT * FROM module WHERE name = 'Riji' LIMIT 1
して最初の要素を取るという至極シンプルなコードになります。
必要があればTengとインターフェースを揃えるためにselectメソッドのwrapperとしてsearch/singleメソッドを用意すると良いでしょう。
my $module = $db->select(module => {
name => 'Riji',
}, {
limit => 1,
})->first;
say '$module->name: ', $module->name; ## Riji
say '$module->author->name: ', $module->author->name; ## SONGMU
prefetchもサポートしています。
prefetchした場合とそうでない場合で返り値のインターフェースが変わらないので、
開発時はN+1クエリを気にせずにゴリゴリ書いて、あとでprefetchを効かせてN+1クエリを潰すということができます。
my $author = $db->select(author => {
name => 'songmu',
}, {
limit => 1,
prefetch => [qw/modules/],
})->first;
say '$author->name: ', $author->name; ## SONGMU
say 'modules[]->name: ', $_->name for $author->modules; ## DBIx::Schema::DSL, Riji
より詳しい話が聞きたい方は Gotanda.pm #7 vs Yokohama.pm #13 にきてください。本当にたよりがいのあるORMをお見せしましょう…!!
まとめ
AnikiはTengの思想をベースにリレーションシップのサポートを取り入れたORMです。
私のプロジェクトでは既に取り入れてますが、Tengと殆ど同じ感覚で開発でき、非常に便利だと思います。
ぜひ、使ってみてください!
明日は punytan さんです。よろしくおねがいします!