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

第38回DBIx::Class:拡張性の高さが売りではありますが

国内では微妙な立ち位置に

ずいぶん間が空いてしまいましたが、今回はデータベース話の3回目として、DBICことDBIx::Classについてまとめてみます。DBICは、海外ではMooseCatalystと並ぶモダンPerl界の三種の神器のひとつとしていまも広く宣伝されていますが、国内では、当初こそClass::DBIからの乗り換えを強力に推進する流れが見られたものの、最近ではあまり名前を聞くこともなくなり、むしろDBICからの脱却が潮流になっているかの印象を受けることさえあります。いったい何がどうなっているのか、例によって歴史を追いかけながら見ていきましょう。

もともとはオブジェクトを永続化するためのもの

DBICの立ち位置を理解するには、まずはその先駆けとなったClass::DBIがどういうものであったかを理解しておく必要があります。

連載第36回でも紹介したように、マイケル・シュワーン(Michael Schwern)氏が1999年12月にリリースしたClass::DBIは、その前年にリリースされたDBIx::Recordsetなどと並んで、DBIの拡張としては最初期のものにあたります。DBIx::Recordsetや、やや遅れて登場したDBIx::Abstractは、インタフェースこそ抽象化されて生のSQLを書く必要はなくなったものの、取り出したデータは生の配列やハッシュのリファレンスに格納されていましたが、Class::DBIは、ちょうどこの時期Perlのオブジェクト指向プログラミングが本格化してきたという事情もあって(ダミアン・コンウェイ氏の『Object Oriented Perl(オブジェクト指向Perlマスターコース⁠⁠』が出版されたのは1999年8月(邦訳は2001年)のことでした⁠⁠、インタフェースの改善にとどまらず、取り出したデータもオブジェクトになっているのがひとつの特徴でした。

もう少し正確にいうと、初期のClass::DBIは、連載第19回で取り上げたKiokuDBと同じく、あくまでもオブジェクトを永続化するための手段としてデータベースを使っていただけで、主眼はあくまでもオブジェクトにありました。KiokuDBと違ってテーブルとクラス、カラムとアクセサ/ミューテータは一対一で対応していましたが、Class::DBIを継承するパッケージに書くのはあくまでもオブジェクトの内部データをいじるためのアクセサ/ミューテータであって、テーブルのスキーマそのものではありませんでしたし、背後にあるIma::DBIを通じて生のSQLを発行する仕組みこそあったものの、通常は極力SQLを意識させないようになっていました(実際、生のSQLを発行する場合はDBIとほぼ同レベルのことしかできなかったため、Class::DBIのうまみはほとんど享受できませんでした⁠⁠。名前こそDBIとついていましたが、これはあくまでもクラスにDBIとの連携機能を追加したもの、という位置づけであって、既存のデータベースを自由に操るためのものではなかったわけです。

もっとも、たとえばウェブのフォームから送られてきたデータをそのまま保存したいだけなら、リレーショナルデータベースの力はそれほど必要としません。リレーショナルデータベースの皮をかぶったカード型(簡易)データベース的な使い方しかしていないのであれば、Class::DBIに用意されている基本的なメソッドだけですべての処理をまかなうこともできました(実際、Class::DBIの初期の実装はDBD::CSVでテストされていました。想定されていた用途はそれで足りるくらいのものだったわけです⁠⁠。

ただし、Class::DBIはあまり作り込まれていなかった分、拡張の余地もたくさんありました。2001年6月にトニー・ボウデン(Tony Bowden)氏がClass::DBI::mysqlと題してMySQL用の拡張をリリースしたのを皮切りに、宮川達彦氏のClass::DBI::ExtensionClass::DBI::Replicationなど、Class::DBIとデータベースの連携を強化するモジュールが少しずつ増えていきます。

この傾向は2001年9月にボウデン氏がメンテナを引き継いでからはさらに顕著になりました。もともとは「Simple Object Persistance(簡単なオブジェクトの永続化⁠⁠」と銘打たれていたタイトル行が「Simple Database Abstraction(簡単なデータベースの抽象化⁠⁠」に変わったのは2002年11月にリリースされた0.90でのことでしたが、それと前後してリレーションまわりの対応が大幅に強化されていますし、カラムの型制約を指定したり、複数の条件をくみあわせたフィルタリングができるようにもなりました。最初にデータベースありきという流れのひとつの象徴として、2002年8月には池邉智洋氏のClass::DBI::Loaderというデータベースのスキーマから自動的にクラス定義を行ってくれるモジュールも登場しています。

その後もユーザ数の増加にともなって次々に新しいモジュールが登録され、2004年頃までにClass::DBIはこの分野におけるデファクトスタンダードとして、CPANでもっとも人気のあるモジュールのひとつに数えられるようになっていました。2004年1月に登場したApache::MVC、のちのMaypoleというウェブアプリケーションフレームワークでは、ビューを司るTemplate Toolkitと並んで、Class::DBI(::Loader) がモデル層を抽象化する重要なパーツとなっています。

NEXTの時代

Maypoleは、よくある管理画面のように、お決まりのフォームにお決まりのデータベース、テンプレートも基本的な構造はほとんど同じ、というものを、お決まりのモデル、お決まりのテンプレートエンジンを使ってさくっと仕上げる場合にもっとも力を発揮するように書かれていました。原作者であるサイモン・カズンズ(Simon Cozens)氏は、Perl Foundationからの助成金を受けて集中的に開発できたという事情もあって、2004年7月にリリースした1.7で一通りの機能は揃ったと判断し、事実上の開発終了を宣言するのですが、そのあとを継いでメンテナになったゼバスティアン・リーデル(Sebastian Riedel)氏が、保守管理だけしてくれればというカズンズ氏の意向に反して、Maypoleに連載第2回でも紹介したNEXTを組み込んでより汎用的な、拡張性の高いものにしようとしたため、カズンズ氏らから「その変更は適切なデフォルトを重視するMaypoleの本質を変えてしまう」と猛反発を受け、改造版のMaypoleにCatalystという別の名前をつけて独立せざるをえなくなる、という事件が起こります。その騒動の余波をまともにかぶったのがClass::DBIでした。

Class::DBIはもともとそれほど複雑なことができるようには作られていなかったため、銘々がプラグインを書いて必要な機能を追加していたのは先にも書いた通りですが、Class::DBIは多重継承をよしとする時代の産物であったため、多いときでは本体だけで5つものクラスを継承していたうえ、初期の拡張のなかには機能を追加するために本体を継承するものもあれば、継承関係は無視して特定のメソッドをミックスインするものもある、という具合で、利用したいプラグインの数が増えていくにつれ安全確実な機能拡張がむずかしくなる、という問題が知られていました。

ボウデン氏がメンテナに就任した理由のひとつにはこの多重継承の軽減があったそうで、実際、2001年10月にリリースされたバージョン0.35ではフック(トリガ)機構を導入して継承に頼らない拡張を可能にしたほか、2004年4月にリリースされた0.96で一通りの機能追加が終わったあとは内部をさらにリファクタリングしてプラグインを書きやすくする計画もあったようなのですが、こちらはなかなか実現されず、結果的には1年以上もリリースが滞ることになってしまいます。そのため、Catalystと同じくらい柔軟な拡張性や頻繁な更新を求めるリーデル氏らと、安定性を重視し過剰な拡張性は不要とするボウデン氏らとの間で意見の衝突が目立つようになり、最終的には業を煮やしたマット・トラウト(Matt S Trout)氏がClass::DBI::Sweetというベタープラクティス集的なパッケージに続いて(2005年5月⁠⁠、実験的なプロジェクトとして、Class::DBIとの互換性を維持しつつもNEXTを使って拡張性を高めたDBIx::Classの開発を始めます(2005年8月⁠⁠。

そのこと自体は多重継承と委譲のどちらがすぐれているかという興味深い議論の成果でもあったのですが、ちょうどこのときリーデル氏の一連の発言にうんざりしたボウデン氏が冷却期間をおくためメーリングリストやWikiを閉鎖するという事件が起こったこともあって、Catalyst界隈を中心にClass::DBI(::Sweet) からDBICへの乗り換えが始まります。リーデル氏はやがてCatalystチームからも追われて、トラウト氏を中心とした集団指導体制に移ることになるのですが、その頃にはもうCatalystとDBICはほとんど不可分のペアとして宣伝されるようになっていました。国内でもこの頃はまだClass::DBI時代のバッドノウハウを解消するものとしてDBICへの好意的な発言がよく見られたものです。

DBICもいずれはMooseに

もっとも、DBICの開発はかならずしも順調に推移してきたわけではありません。

DBICはもともとClass::DBIの拡張性の問題を解決するためのプロジェクトだったこともあって、プライマリキーの扱いから例外やアクセサまわりの処理まで、すべてが拡張として実装されているのですが(スキーマを書く際のベースとして使われるDBIx::Class::Coreモジュールの仕事は、いくつかのデフォルトコンポーネントを読み込むことだけです⁠⁠、同じくNEXTをベースにして拡張性を高めたはずのCatalystのプラグイン機構が結局は破綻してMooseで書き換えられてしまったように、DBICのほうも、NEXTからClass::C3への移行こそいち早く対応できたものの(2005年11月⁠⁠、その後、実験的に追加されたDBIx::Class::ResultSetManagerの失敗などもあって、次のメジャーリリースとなる0.09系列ではMoose化されるという話がもう何年も前から公言されています。

もっとも、DBICの履歴を見ると、0.01から0.07までは2~4ヶ月程度のスパンでリリースされてきたのが、0.08についてはほぼ1年かかっていますし(2007年6月⁠⁠、その次は、順当に行けば0.09になりそうなものだったのですが、実際には2年近くかけて0.081というバージョンになり(2009年4月⁠⁠、いまもなお0.081系列のリリースが続いています。この次が0.09になるのか、それとも0.082のようなバージョンになるのかはまだわかりませんが、当面はやきもきしながら見守る時間が続くのでしょう。

ただし、Moose化に向けた実験が始まっていないわけではありません。たとえば、2010年5月にリリースされたDBIx::Class::MooseColumnsというモジュールを使うと、このようなResultクラスが

package MyApp::Schema::Result::User;
use base qw/DBIx::Class::Core/;

__PACKAGE__->table('users');
__PACKAGE__->add_column(id => {
    data_type => 'integer',
    is_auto_increment => 1,
});
__PACKAGE__->add_column(name => {
    data_type => 'varchar',
    size => 60,
});
__PACKAGE__->set_primary_key('id');

1;

こう書けるようになります。

package MyApp::Schema::Result::User;
use Moose;
use DBIx::Class::MooseColumns;
use namespace::autoclean;

extends 'DBIx::Class::Core';

__PACKAGE__->table('users');

has id => (
    isa => 'Int',
    is => 'rw',
    add_column => {
        data_type => 'integer',
        is_auto_increment => 1,
    }
);
has name => (
    isa => 'Str',
    is => 'rw',
    add_column => {
        data_type => 'varchar',
        size => 60,
    }
);

__PACKAGE__->set_primary_key('id');
__PACKAGE__->meta->make_immutable(inline_constructor => 0);

DBIx::Class::MooseColumnsはまだアルファ版ということで、このAPIがそのまま次のDBICで採用される保証はありませんし、記述が特に簡潔になるわけでもないのですが、素のDBICはSQL::Translator経由でスキーマを自動生成するときなどを除いてdata_typeなどの属性は見ないので、ここにMooseの型制約を持ち込むことは、よりデータベースに寄り添った実装をしたい人にとっては理にかなったことでしょうし、ロールやメソッドモディファイアを使った拡張が可能になれば拡張の柔軟性や信頼性は増すことでしょう(ちなみにDBICのCookbookにはすでにClass::Method::Modifiersを使った拡張例が掲載されています⁠⁠。DBICは単なるリレーショナルデータベースのラッパであって、せいぜいinflate/deflateに対応してくれればよい、という使い方をする人にとってはありがたみの薄い話でしょうが、DBICが扱うのはあくまでもオブジェクトでありモデルであって、背後のデータベースは永続化の一手段に過ぎないとみなす人にとっては、この変更は大きな意味を持つものになるかもしれません。

単なるデータベースのラッパとして使うのはもったいない

Catalystの世界では以前、DBICのモデルがあまりにCatalyst(のウェブコンテキスト)に密結合しすぎているため、CLIなどから使いづらい、という問題提起がされて、DBICモデルを切り離すためにCatalyst::Model::Adaptorのようなモジュールが生まれたのですが、いくらコンテキストを切り離したところで、ウェブコントローラやCLI、テストなどで同じクエリを繰り返し書いているようでは意味がありません。複雑なクエリやよく使うクエリは、⁠DBICのメソッドではなく)そのクエリ自体をテストできるようにするためにも、何らかの方法でラップしておきたいものです。

連載第8回で紹介したReactionでは素のDBICにもう一枚インタフェースモデルというものをかぶせて使っていましたが、DBICはもともと拡張性を重視して書かれているのですから、アプリケーション固有のインタフェースをDBICモデルに追加する方法もとれるはずです。

たとえば、DBIx::Class::ResultSetManager自体は廃止が決まったものの、リザルトセットに独自のメソッドを生やすことが否定されたわけではありません。不安定なサブルーチンアトリビュートを使わず、ふつうにResultSetクラスを書くようにしてほしいというだけのことなので、たとえばこのようなリザルトセットクラスを用意して、

package MyApp::Schema::ResultSet::User;
use strict;
use warnings;
use base qw/DBIx::Class::ResultSet/;

sub names_that_start_with {
    my ($self, $first_letter) = @_;
    my @names;
    my $rows = $self->search({name => {like => "$first_letter%"}});
    while (my $row = $rows->next) { push @names, $row->name };
    return @names;
}

1;

リザルトクラス(MyApp::Schema::ResultSet::User)の最後のほうに次の一行を追加すると、

__PACKAGE__->resultset_class('MyApp::Schema::ResultSet::User');

このような書き方で特定の頭文字で始まるユーザ名の一覧がとれるようになります(詳しくはCookbookをご覧ください⁠⁠。

my @names = $schema->resultset('User')->names_that_start_with('I');

また、2009年2月にリリースされたバージョン0.08099_07からは、複数のテーブルにまたがるようなSQLでもよしなに処理できるように、ビューの対応が追加されています。以下の例はもちろん標準のリレーションでも対応できますが、ビューを用意することでより直感的なオブジェクトが生成できるようになります(こちらもCookbookにもう少し詳しい説明が載っています⁠⁠。

package MyApp::Schema::Result::MyView;
use strict;
use warnings;
use base qw/DBIx::Class::Core/;

__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('my_view');
__PACKAGE__->add_columns(qw/id name email/);
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q/
    select u.id as id, u.name as name, e.email as email
    from users u, emails e where u.id = e.id
/);

1;

用途にあったものを使ってください

DBICにはここで紹介した以外にもさまざまな機能が用意されています。最近ではかなり複雑なことであってもDBICのレベルで表現できるようになっていますし、個々のストレージの違いなども積極的に吸収していますから、特定のデータベースを強要したくない一般ユーザ向けのアプリケーションをつくるときや、特定のデータベースへの乗り換えを強制することができない受託系の仕事をしている場合などはDBICの汎用性がありがたく思えることも多いでしょう。また、オープンソースであれクローズドソースであれ、世界中から人材を集めて開発するような場合も、DBICはデファクトスタンダードとしての強みを発揮できるはずです(実際、海外の人材募集ではDBICが必須技能のひとつに指定されていることもあります⁠⁠。

その一方で、DBICはあまりに大きく、複雑になりすぎました。一般ユーザ向けのアプリケーションに同梱しようにも、CPANを使えない環境ではインストールにもひと苦労ですし、対応しているストレージの多さは逆に最大公約数的なことしかできない、という意味にもなります。DBICが内部的に発行するSQLはかならずしも十分に最適化されているとは限りませんし、ねらった通りのタイミングで発行されるとも限りません。個人の開発環境ではそれほど気にならなくても、大規模サービスの現場やリソースの限られた共有環境ではその重さがはっきり目立つこともありますから、特定の環境でカリカリにチューニングしたければほかの解決策を選んだほうがよいでしょう。DBICであれなんであれ、ご自分の用途にあったものを使ってください。

次回はデータベース話の締めくくりとして、DBIC以後に登場したいくつかの選択肢について取り上げてみます。

おすすめ記事

記事・ニュース一覧