今日のCPANモジュール(跡地)

宣伝と注意書き

このサイトが元になったCPANモジュールガイドという本を書きました。

DBICについては取り上げませんでした。重量級すぎるということと、DBへのアクセスについてはDBIを取り上げているため、割愛しました。実際、どのタイミングでSQLが発行されるのかわかりにくいので筆者は使わなくなりました。

使わなくなって久しいのですが、このページの記述はだいぶ古くなっている可能性があります。

2007-04-18

use DBIx::Class;

今回は、前々回 DBIx::Simple を紹介した際に予告した本格 O/R マッパーモジュール DBIx::Class (略称 DBIC)を紹介します。

Perl の世界では、昔から Class::DBI (略称 CDBI)という優れた ORM モジュールがあり、プロダクションでもがりがり使われてきました。CDBI をより便利にする拡張もたくさん現れ、中でも Class::DBI::Sweet というモジュールがよく使われていました。

この CDBI::Sweet の作者が、CDBI の拡張という形ではできないことまでやりたくなってついに一から書き直したのが DBICです。今では多くの開発者が加わり、(CDBI ほど枯れてませんが)プロダクションで使えるレベルになったと思います。先日の YAPC::Asia でも いくつかセッションが ありました。

CDBI、DBIC 以外の ORM モジュールもありますが、現時点ではこれが一番おすすめできます。現実的(複雑なことも可能・カスタマイズ可能)であり、それらを満たす範囲で十分わかりやすい API。今から Perl 始める人は幸せだなと。

使い方

例として、SQLite でまたドラクエの呪文 DB を作ることにします。

DBIC は DBIx::Simple と違い、事前に「データベース」に対応したクラス(スキーマクラス)や、「テーブル」に対応したクラスを用意しておき、プログラムではそいつらを通して DB にアクセスします。この対応クラスを用意するには手動と自動の2種類ありますが、半自動(自動+手動で微調整がおすすめです。この方法を簡単に解説します。

スキーマクラス

プロジェクト名が Magic だった場合、スキーマクラスは Magic::Schema というパッケージ名にする場合が多いです。このファイルから書いていきます。といっても書く量はわずかです。lib/Magic/Schema.pm として以下のようなコードを保存します。

package Magic::Schema;
use strict;
use base qw/DBIx::Class::Schema::Loader/;

__PACKAGE__->loader_options(
    debug => 1,
    exclude => qr/^sqlite_/,
);

1;

DBIx::Class::Schema::Loader を継承し、コードは loader_options を指定するくらいです。ちなみに手動の場合は DBIx::Class::Schema::Loader ではなく DBIx::Class::Schema を継承します。

プログラムから利用

こいつを実際にプログラムで使うには、以下のようにします。

use lib 'lib';

# スキーマクラスを use し、
use Magic::Schema;

# スキーマクラスのインスタンスを作成
my $schema = Magic::Schema->connect('dbi:SQLite:dbname=db/spell.db');

# ResultSet を作成し全レコード取得したり...
my $rs = $schema->resultset('Spell')->search;

# 件数を出力したり...
warn $rs->count, " rows ---";

# word、descフィールドを出力しながらイテレートしたり...
while (my $row = $rs->next) {
    warn sprintf "* %s: %s", $row->word, $row->desc;
}

これを test.pl とかで保存して、perl test.pl してみます。

Magic::Schema::ClassMap->load_components("PK::Auto", "Core");
Magic::Schema::Spell->load_components("PK::Auto", "Core");
Magic::Schema::ClassMap->table("class_map");
Magic::Schema::ClassMap->add_columns(
  "id",
  { data_type => "INTEGER", is_nullable => 0, size => undef },
  "name",
  { data_type => "VARCHAR", is_nullable => 0, size => 255 },
);
Magic::Schema::ClassMap->set_primary_key("id");
Magic::Schema::Spell->table("spell");
Magic::Schema::Spell->add_columns(
  "id",
  { data_type => "INTEGER", is_nullable => 0, size => undef },
  "word",
  { data_type => "VARCHAR", is_nullable => 0, size => 255 },
  "class_id",
  { data_type => "INTEGER", is_nullable => 0, size => undef },
  "desc",
  { data_type => "VARCHAR", is_nullable => 0, size => 255 },
  "ctime",
  { data_type => "DATETIME", is_nullable => 0, size => undef },
);
Magic::Schema::Spell->set_primary_key("id");

13 rows ---
* メラ: 火の玉
* ギラ: 炎
* イオナズン: 爆発で敵全体を攻撃
* ホイミ: HPを回復する
つづく

debug を ON にしているので、Loader が DB から取得できる内容を元にテーブルの対応クラスをどう作っていったかがわかります。

スキーマクラスが Magic::Schema の場合、各テーブルを表すクラスは Magic::Schema::*の名前空間になります。なので spell テーブルは Magic::Schema::Spell として、class_map テーブル はMagic::Schema::ClassMap として、DB から取得できる内容を元に PK やフィールド名、型などが DBIC語で表現されています。

作るといってもメモリ上に作るだけで、lib/Magic/Schema/Spell.pm などのファイルが作られるわけではありませんが、Loader を使わない場合はこれを手動で書く必要があります。フィールドが多いとだるいので、自動でやってくれるのがだいぶ楽です。

使い方の流れが見えたところで、以下の順序で例をもう少し詳しく説明していきます。

  1. スキーマクラスの設定
  2. 設定の微調整(リレーションの設定)
  3. いろいろな select, update, insert, delete

スキーマクラスの設定

スキーマクラスに書くコードは loader_options くらいです(その他の内容)。その loader_options に指定できる内容はここに列挙されていますが、よく使うのを説明します。

DBIC コンポーネント

DBIC を拡張する方法の基本はこのコンポーネントを追加する方法です。毎回追加してもいいくらい便利なコンポーネントを紹介してみると

と、コンポーネントは DBIx::Class::なんちゃら という形で CPAN に上げられています。例の3つをロードさせるには、以下のように components に「なんちゃら」部分を並べます。

__PACKAGE__->loader_options(
    debug => 1,
    exclude => qr/^sqlite_/,
    components => [qw(
        InflateColumn::DateTime
        AsFdat
        UTF8Columns
    )],
);

設定の微調整(リレーションの設定)

自動だけで足りないのは、主にリレーションの設定です。(自動でさせる機能もありますが DB のタイプで対応してなかったり、思ったようにいかない場合もあるので手動で設定する方がいい)

例えば呪文マスタと分類マスタは N:1 の関係で、具体的には spell テーブルの class フィールドが class_map テーブルの id の値なのですが、このリレーションを設定してみます。

lib/Magic/Schema/Spell.pm として以下の内容を保存します。

package Magic::Schema::Spell;
use strict;

__PACKAGE__->belongs_to(
    class => 'Magic::Schema::ClassMap'
);

1;

belong to(属する)という関係で classフィールドを分類マスタ(Magic::Schema::ClassMap)に結びつけています。ここで Magic::Schema::Spellとして手動で書きましたが、この内容が Loader がメモリ上で作成した同名のクラスにうまいことマージされます。

先ほどの test.pl の while 部分を以下のようにしてみてください。

while (my $row = $rs->next) {
    warn sprintf "* %s: [%s] %s", $row->word, $row->class->name, $row->desc;
}

# 結果 ---

* ギラ: [攻撃] 炎
* イオナズン: [攻撃] 爆発で敵全体を攻撃

リレーションの設定の書き方はこの POD に書いてあります。設定できる関係は belongs_to 以外に、逆から見た has_many などが用意されています。また、オプションでもっと複雑なリレーションの書き方ができたりもしますが、説明はこのくらいにしておきます。

どんな SQL が作成されてるか

デバッグの仕方はこの POD に書いてありますが、簡単なのは

export DBIC_TRACE=1

と DBIC_TRACE 環境変数に1を入れてから perl test.pl することです。SQL 文と、バインドされる値が出力されながら進みます。

SELECT COUNT( * ) FROM spell me: 
13 rows ---
SELECT me.id, me.word, me.class, me.desc, me.ctime FROM spell me: 
SELECT me.id, me.name FROM class_map me WHERE ( ( ( me.id = ? ) ) ): '1'
* メラ: [攻撃] 火の玉

・・・きっと想像したのとは違う SQL になってると思います。DBIC では SQLの発行を本当に必要なぎりぎりまで伸ばし、またその時々で必要最低限のレコードだけを取得するよう工夫されてたります。もちろんオプションで指示することで SQL を調整可能です。

いろいろな select, update, insert, delete

サンプルのように、データにアクセスするには、

# スキーマクラスを use し、
use Magic::Schema;

# スキーマクラスのインスタンスを作成
my $schema = Magic::Schema->connect('dbi:SQLite:dbname=db/spell.db');

# ResultSet を作成し...
my $rs = $schema->resultset('Spell')->search;

と進め、スキーマクラスのインスタンスを作成するには connect() を使うのが普通です。引数は DBI の connect() と一緒です。(さらに便利に拡張されてます

DBIC では ResultSet というオブジェクトでデータにアクセスします。ResultSet を取得するには、スキーマの resultset() メソッドなどを使います。resultset() は、spell テーブル(Magic::Schema::Spell)なら Magic::Schema:: 部分を省略した部分(Spell とか ClassMap とか)を指定します。

上の例では取得した ResultSet に search() メソッドで全レコードを取得していますが、他のメソッドは DBIC::ResultSet に列挙されています。

この ResultSet がポイントで、DBIC が CDBI とはまったく違うところであり、工夫されてます。ほとんどのメソッドが ResultSet を返すのが特徴で、メソッドを単独で使うこともできればつなげて使うこともできます。この ResultSet が、データが必要になるぎりぎりまで問い合わせを解釈する役割を果たしています。

全部は説明できませんが感じがつかめる程度に例をあげてみます。

# 一つのレコードを特定
my $id3 = $schema->resultset('Spell')->find(3);
warn "ID 3:" . $id3->word if $id3;


# 分類が4,5,6のものを作成日の降順で、3行のみ
my $class4 = $schema->resultset('Spell')->search({
    class => [4, 5, 6],
}, {
    order_by => ['ctime desc'],
    rows     => 3,
});


# パルプンテの説明を変える
$schema->resultset('Spell')
        ->search({ word => "パルプンテ" })
        ->update({ desc => "何が起きるかわからない。"
                         . "携帯版ドラクエ2だといいことしか起きなかった" });


# ルカニ系削除
$schema->resultset('Spell')->search({
    word => { like => "ルカ%" },
})->delete;


# 項目追加
$schema->resultset('Spell')->create({
    word  => "ルカナン",
    class => 3,
    desc  => "敵一グループの防御力を下げる",
});


# 名前に「ラ」説明に「い」を含む呪文の最初の一つのみ取得
my $class3 = $schema->resultset('Spell')->search_like({
    word  => "%ラ%",
    desc  => "%い%",
})->first;

どうでしょう。ResultSet のメソッドやオプション、メソッドチェーンや値の取得あたりの使い方のイメージが伝わったでしょうか。

More

何でもできそうですが、現実の案件ではだいたい基本機能じゃ足りなくなったりします。しかし、カスタマイズが容易でいろいろな方法が用意されているのも DBIC の特徴で、具体的には以下のような点に手を入れます。