Class::DBI - オブジェクトのシンプルな永続

この文書はClass::DBIの初期バージョンのドキュメントを訳したもので、現在のバージョンとは整合性がありません。CPAN から最新のバージョン を取得して内容を確認してください。

名前

  Class::DBI - オブジェクトのシンプルな永続


概要

  package Film;
  use base qw(Class::DBI);

  # Class::DBI にあなたのクラスのことを教えてあげる
  Film->table('Movies');
  Film->columns(All     => qw( Title Director Rating NumExplodingSheep ));
  Film->columns(Primary => qw( Title ));
  Film->set_db('Main', 'dbi:mysql', 'me', 'noneofyourgoddamnedbusiness',
               {AutoCommit => 1});

  #-- 一方、スクリプトの方で! --#
  use Film;

  # "Bad Taste" 用の映画エントリを新しく作る
  $btaste = Film->new({ Title       => 'Bad Taste',
                        Director    => 'Peter Jackson',
                        Rating      => 'R',
                        NumExplodingSheep   => 1
                      });
  
  # 'Gone With The Wind' のエントリをデータベースから取得する
  my $gone = Film->retrieve('Gone With The Wind');
  
  # なんと新たな場面が見つかって、Scarlet と羊の猥褻シーンだった!
  $gone->NumExplodingSheep(5);
  $gone->Rating('NC-17');
  $gone->commit;

  # 'Bladerunner' エントリを取り出す
  my $blrunner = Film->retrieve('Bladerunner');

  # 'Bladerunner' のコピーをつくって、ディレクターズ・カットのエントリをつくる
  my $blrunner_dc = $blrunner->copy("Bladerunner: Director's Cut");

  # Ishtar はもうなくてもいい
  Film->retrieve('Ishtar')->delete;

  # PG のレイティングを持つ映画を全部探す
  @films = Film->search('Rating', 'PG');

  # Bob が監督をした映画を全部探す
  @films = Film->search_like('Director', 'Bob %');


説明

わたしはSQLが嫌い。あなたもSQLが嫌い。みんなSQLが嫌い。でも、悲しいかな、 オブジェクトを永続させるニーズはしょっちゅうあります。そんなとき、たいがいSQL データベースがもっとも柔軟なソリューションになります。

このモジュールは、少しのSQLとDBIの知識で、効率的で、シンプルで、拡張性の高い 永続オブジェクトをセットアップするためのものです。

スキーマを用いて、クラスのデータへのアクセサを自動的に提供します。 これらのアクセサによって、データベースへのアクセスを制御します。

セットアップの方法

あなたのクラスを永続化させるとても簡単な方法を示します。 個々のメソッドについての詳細は、あとで。

データベースをセットアップする。

セットアップされたデータベースがあって、DBI.pm と そのデータベースエンジンに必要な DBD:: ドライバモジュールがインストールされている 必要があります。詳細は DBI と、データベースのドキュメントを読んで下さい。

困った時は、DBD::CSV でも大丈夫です。

オブジェクトを格納するテーブルをセットアップする。

Class::DBI は、シンプルな 1クラス/1テーブルモデルで動作します。 クラス用のテーブルをセットアップするのはあなたの義務です。このプロセスを自動化するのは、複雑になりすぎるでしょう (だれかが私を説得するならやりますけど)。

さきほどの映画の例でいうと、こんな感じのテーブルを作成する必要があります。

  CREATE TABLE Movies (
         Title      VARCHAR(255)    PRIMARY KEY,
         Director   VARCHAR(80),
         Rating     CHAR(5),    /* すくなくとも 'NC-17' にフィットする */
         NumExplodingSheep      INTEGER
  )

Class::DBI を継承する。

これは、base.pm を使うほうが、@ISA に追加する方法より好ましいです。 なぜなら、あなたのクラスは Class::DBI から protected のフィールドを継承する必要があるからです。 それに、クラスを疑似ハッシュ(pseudo-hash) で実装する場合、これが重要です。
  package Film;
  use base qw(Class::DBI);

カラムを宣言する。

columns() を使います。フィールドの名前はデータベースのカラム名に1対1で対応しなくてはなりません。 Class::DBI は、(Class:Accessor 経由で)この情報からアクセサを作ります。
  Film->columns(All => qw( Title Director Rating NumExplodingSheep ));

カラム宣言を効率的に行う方法をもっと知りたければ、"カラムのlazyな生息" をみてください。

テーブル名を宣言する。

あなたのオブジェクトをどのテーブルにいれるか、Class::DBI に伝えます。 これは先ほどつくったテーブルのことです。
  Film->table('Movies');

どのフィールドが Primary Key かを宣言する。

フィールドのうちひとつは、それぞれのオブジェクトの識別子である必要があります。 これは、データベースでの PRIMARY KEY になるでしょう。Class::DBI は この情報から、格納されたオブジェクトに関して、適切なSQL文を生成します。
  Film->columns(Primary => 'Title');

データベース接続を宣言する。

Class::DBI はどうやってデータベースにアクセスするか知る必要があります。 これは DBI の接続をセットアップしておこないます。セットアップは set_db() メソッドを呼び出して、 'Main' という名前のデータベース接続を定義して行います。
  Film->set_db('Main', 'dbi:mysql', 'user', 'password', {AutoCommit => 1});

set_db() は Ima::DBI から継承されています。詳細はモジュールの man page をみてください。

XXX これはもうちょっとシンプルにすべきでしょう。set_db_main() みたいにするとか。

完了。

すべておわりです! これでコンストラクタ(new(), copy(), retrieve())、 デストラクタ (delete())、それにすべてのアクセサ、あとは Class::DBI が提供するガラクタが使えるようになりました。 何か新しくオブジェクトをつくって、いじくってみてください。 オブジェクトが何かするとテーブルがどうなるか、オブジェクトが格納され、変更され、 消去されるのをみてください。

すばらしいでしょう? このモジュールを崇拝して下さい。


メソッド

以下に示すメソッドは、オブジェクトのデータ構造が ハッシュもしくは疑似ハッシュを使っている、という前提を使用しています。

生と死 - コンストラクタとデストラクタ

以下は、格納されるオブジェクトを生成、取得、消去するためのメソッドです。 これでなんでもできる、というわけではないので、オーバーライドする必要があるかも知れません。

new

    $obj = Class->new(\%data);

新たにオブジェクトを作ってデータベースにいれる、コンストラクタです。 %data はオブジェクト、そしてデータベースにいれる、初期情報です。 %data のキーは、オブジェクトのカラムにマッチし、値はそのフィールドの初期値になります。

$obj はハッシュリファレンスからつくられる、Class のインスタンスです。

  # "Bad Taste" 用の映画エントリをつくる
  $btaste = Film->new({ Title       => 'Bad Taste',
                        Director    => 'Peter Jackson',
                        Rating      => 'R',
                        NumExplodingSheep   => 1
                      });

もし PRIMARY KEY のカラムが%data にない場合、new() は、そのカラムのデータは生成される、と想定します。 sequence() がこのクラスに対して指定されていれば、それを使います。 そうでなければ、PRIMARY KEY は AUTO_INCREMENT 制約があるのだと想定し、それを使おうとします。

クラスが外部クラスに対して hasa() で関係を定義されている場合、 new() に そのキーの値としてオブジェクトを渡すことができます。 Class::DBI はちゃんとまっとうに扱います。

retrieve

  $obj = Class->retrieve($id);

ID を受けて、データベースからそのIDを持つオブジェクトを取得します。
  my $gone = Film->retrieve('Gone With The Wind');

copy

  $new_obj = $obj->copy($new_id);

$obj のコピーを、メモリにもデータベース上にも作成します。 違うのは、$new_obj は主ID に $new_id の値を持つ、ということだけです。
    my $blrunner_dc = $blrunner->copy("Bladerunner: Director's Cut");

delete

  $obj->delete;

このオブジェクトを、データベースからもメモリからも削除します。 これを呼び出したら、$obj はもう使えません。

アクセサ

Class::DBI は Class::Accessor を継承しているので、 あなたのつくったサブクラスに対して、全てのカラムへのアクセサメソッドを 提供します。アクセサが提供する get() と set() のメソッドをオーバーライドして、 データベースのトランザクションを自動的に制御できるようにしています。

アクセサの動きには2つのモードがあります。手動コミットとオートコミットです。 DBI の手動/オートコミットに似ているのですが、それと同じように実装されているわけではありません。 簡単に言うと ... オートコミットモードでは、変更するためのアクセサがよびだされるたび、 その変更はただちにデータベースに書き込まれます。 そうでない場合、つまりオートコミットが off の場合、commit() が明示的に呼ばれるまで、変更はデータベースには書き込まれません。

手動コミットの例はこちらです。

    # NumExplodingSheep() と Rating() は メモリ上で
    # データ変更するだけで、データベースには書き込まれない。
    # commit() が呼ばれると、データベースに1度に書き込まれる。
    $gone->NumExplodingSheep(5);
    $gone->Rating('NC-17');
    $gone->commit;

こちらはオートコミットの例。

    # このオブジェクトについては、オートコミットを on にする。
    $gone->autocommit(1);

    # それぞれのアクセサ呼び出しによって、新しい値がすぐに書き込まれる
    $gone->NumExplodingSheep(5);
    $gone->Rating('NC-17');

手動コミットのほうが、たぶんオートコミットよりも効率的でしょうし、 rollback() によって、変更を取り消す安全も提供されます。 オートコミットはプログラマにとっては楽でしょう。

オブジェクトが破棄されたとき(スコープ外にでるか、プログラムが終了した場合)に、 変更がコミットされず、ロールバックもされていない場合、 Class::DBI の DESTROY メソッドが呼び出されて、変更が保存されてないことについて warning を表示します。

autocommit

    Class->autocommit($on_or_off);
    $commit_style = Class->autocommit;

    $obj->autocommit($on_or_off);
    $commit_style = $obj->autocommit;

現在のオートコミット状態へのアクセサ。引数無しで呼ばれると、現状のオートコミット状態を on なら true, off なら false として返します。 引数ありで呼ばれると、オートコミット状態を on または off にセットします。 true 値で on, false 値で off です。 クラスメソッドとして呼ばれると、そのクラスのインスタンスすべてに対して適用されます。 個々のオブジェクトから呼ばれた場合は、クラスの設定をオーバーライドして、そのオブジェクトのコミットのみに適用されます。
  Class->autocommit(1);     # オートコミットはクラスに対して on
  
  $obj = Class->retrieve('Aliens Cut My Hair');
  $obj->autocommit(0);      # このオブジェクトについてはオートコミット off

オブジェクトごとのコミットの設定はデータベースには格納されません。

オートコミットはデフォルト off です。

注意 これは DBI の AutoCommit 属性とは 何の関係もありません

commit

    $obj->commit;

アクセサによる変更をディスクに書き込みます。オートコミットの状態で commit() を呼び出しても構いません。黙ってなにもしないだけです。

rollback

  $obj->rollback;

最後のコミットから現在までの、オブジェクトに関する変更を全て取り消します。 現在は、データベースから値を単純にリロードしてくるだけです。これには、並行処理上の問題があるでしょう。

もしオートコミットの状態でこのメソッドを呼び出すと、例外を投げます。

is_changed

  @changed_keys = $obj->is_changed;

$obj がコミットされていない変更を持っているかどうかを表示します。 変更されたキーのリストを返します。

データベース情報

id

  $id = $obj->id;

オブジェクトのユニーク識別子を返します。これは $obj->get($self->columns('Primary')); と同じです。

table

  Class->table($table);
  $table = Class->table;
  $table = $obj->table;

クラスが格納されるデータベーステーブルへのset/get アクセサ。これは -必ず- セットされなければなりません。

テーブル情報はサブクラスに継承されますが、オーバーライドはできません。

sequence

  Class->sequence($sequence_name);
  $sequence_name = Class->sequence;
  $sequence_name = $obj->sequence;

PRIMARY KEY のシーケンスへの set/get アクセサ。
    Class->columns(Primary => 'id');
    Class->sequence('class_id_seq');

Class::DBI は、オブジェクトが生成されたが、PRIMARY KEY が指定されていない場合、シーケンスを利用して PRIMARY KEY を作ろうとします。

注意: Class::DBI は AUTO_INCREMENT や、それに類するセマンティクスもサポートしています。

columns

  @all_columns  = $obj->columns;
  @columns      = $obj->columns($group);
  Class->columns($group, @columns);

クラスのデータベース上のカラム名へのアクセサ。クラスに対するSQL文を生成するのに使われる。

カラムは特有の使い方によってグループわけすることができ、それによってグループ内のカラムを一度に公理強く読み込むことができる。 これについてのもっと詳しい情報は、"カラムのlazyな生息"をみてください。

「予約された」3つのグループがあります。'All', 'Essential' そして 'Primary' です。

'All' はクラスでつかわれるすべてのカラムです。 セットされない場合、他のグループから自動生成されます。

'Primary' はクラスの単一PRIMARY KEY です。オブジェクトを使用する前に、必ずセットされなければなりません。 (複数のPRIMARY KEYも、最終的にはサポートされることになるでしょう)

    Class->columns('Primary', 'Title');

'Essential' はオブジェクトをロードし、使用するのに最低限必要なカラムの集合です。 オブジェクトが retrieve() されると、このグループ内のカラムのみがロードされます。 あるクラスが、たくさんのカラムをもっているが、たいがいの場合その中のいくつかしか使わないとき、メモリ使用量を削減するといったときに使います。 もしセットしなければ、Class->columns('All') から自動生成されます。

引数を与えない場合、すべてのカラムのリストがほしいものと想定されます。

注意 スカラコンテキストでの動作をどうするか、まだ決めていません。

is_column

    Class->is_column($column);
    $obj->is_column($column);

$column がクラスまたはオブジェクトのカラムであれば、true を返します。

テーブル関係、オブジェクト関係

ひとつのテーブルが別のテーブルを FOREIGN KEY で参照するように、 あるオブジェクトに別のオブジェクトを持たせたいことはよくあるでしょう。 たとえば、映画監督について、もっとたくさん情報を持たせたいとき。 テーブルをつくります...

    CREATE TABLE Directors (
        Name            VARCHAR(80),
        Birthday        INTEGER,
        IsInsane        BOOLEAN
    )

そして、Class::DBI のサブクラスを用意します。

    package Film::Directors;
    use base qw(Class::DBI);

    Film::Directors->table('Directors');
    Film::Directors->columns(All    => qw( Name Birthday IsInsane ));
    Film::Directors->columns(Prmary => qw( Name ));
    Film::Directors->set_db(Main => 'dbi:mysql', 'me', 'heywoodjablowme',
                            {AutoCommit => 1});

これで Film は カラム Director を通して、監督名だけでなく、Film::Directors のオブジェクトを取得できるようになります。 Film に1行追加するだけでできます。

    # Director() は Film::Directors オブジェクトへのアクセサ
    Film->hasa('Film::Directors', 'Director');

これで Film->Director() アクセサは、監督名の代わりに、Film::Director オブジェクトを get/set できるようになります。

hasa

    Class->hasa($foreign_class, @foreign_key_columns);

Class が $foreign_class との関係をもち、$foreign_class の PRIMARY KEY を @foreign_key_columns の中に持っていることを定義します。

@foreign_key_columns の先頭要素の名前で アクセサが生成されます。これによって、$foreign_class のオブジェクトを get/set できます。 Film::Director の例で言うと ...

    # Bad Taste の監督を Peter Jackson を表す
    # Film::Director オブジェクトにする
    $pj     = Film::Directory->retreive('Peter Jackson');
    $btaste = Film->retreive('Bad Taste');
    $btaste->Director($pj);

hasa() は外部クラスの require を行おうとします。 もし require が失敗しても、単純な require ではない (つまり Foreign::Class は Foreign/Class.pm ではない) と判断し、すでにあなたがそのあたりを処理しているものとして、warning を無視します。

@foreign_key_columns をセットアップするのに、columns() を呼び必要はありません。 やっていなければ、hasa() が自動的にやってくれます。

XXX この動作が気持ちいいかどうか、疑問です。将来すこし変わるかもしれません。 アクセサの命名形式があまり自信がないです。

注意 2つのクラスは同じデータベースに存在する必要はありません!

カラムのlazyな生息

Perl の伝統にのっとって、Class::DBI はオブジェクトのロードについてlazy です。 たくさんのオブジェクトを同時に扱うときなど、必要なカラムは少ししかないのに、全部のカラムを取り出すのはメモリの無駄だなあ、と思うときがあるでしょう。

Class::DBI はカラムをグループでとりだします。グループ内の1つのカラムにアクセスすると、グループの他のカラムも使用するだろうと想定して、グループのカラムを全部ロードします。 よって、たとえば Film クラスに NetProfit(純利益) と GrossProfit(総利益) のカラムを追加したいとしましょう。 おそらく、この2つはいつも一緒に利用されるでしょう。ですから ...

    Film->columns('Profit', qw(NetProfit GrossProfit));

これで、次のように呼び出すと、

    $net = $film->NetProfit;

Class::DBI は NetProfit と GrossProfit 両方をデータベースからロードします。 次に GrossProfit() を同じオブジェクトに対して呼び出した場合、データベースを叩く必要はありません。 これによってパフォーマンスを上げることができるかもしれません (YMMV)。

この動作が気に食わないならば、'All' というグループだけつくって、カラムをすべてそこに入れて下さい。そうすれば、Class::DBI はすべて一度にロードします。

データ正規化

SQL はたいがい、case insensitive (大文字小文字の違いは無視する) です。 Perl はたいがい、そうではありません。これによって、データベースから情報を取り出すときに問題が起こることがあります。 Class::DBI はデータのちょっとした正規化をおこないます。

normalize

  $obj->normalize(\@columns);

データベースがカラムの大文字小文字をどのように扱うかの保証は ありません。よって、DBI->fetchrow_hashref() がおかしな大文字小文字 でカラム名(また、先頭につくテーブル名も)を返してくるような問題を防ぐため、 すべてのカラム名を データのキーとして使用する前に、正規化します。

normalize_hash

    $obj->normalize_hash(\%hash);

%hash のすべてのキーを normalize() で正規化します。利便のためのメソッドです。

SQL文を定義する

Class::DBI は Ima::DBI を継承しているので、Ima::DBI スタイルでデータベースやDBIを扱うのが好ましいです。(Ima::DBI の man page を流し読みしてみましょう)

サブクラスによって継承することができるようなメソッドを新しく書くためには、クラスのテーブル名、PRIMARY KEY 名をハードコードしないように注意しなければなりません。 set_sql() を使うと、効率的に、キャッシュされるステートメント・ハンドラを生成することができます。

一般に、set_sql() の呼び出しはこんな感じになります。

    # sql_GetFooBar() を定義する
    Class->set_sql('GetFooBar', <<'SQL');
    SELECT %s
    FROM   %s
    WHERE  Foo = ? AND Bar = ?

これによって、 sql_GetFooBar() というメソッドが定義されます。 引数は sprintf() を通して、SQL文を埋めるのに使われます。

    my $sth = Class->sql_GetFooBar(join(', ', Class->columns('Essential')),
                                   Class->table);

クラスのテーブル名やPRIMARY KEY名をハードコードしないように注意して下さい。代わりに、table() や columns() メソッドを使います。

$db_name が省略された場合、'Main'接続 を使用すると想定します。

検索

いくつかのシンプルな検索用メソッドが提供されています。シリアスな検索ではなく、クラスの可能性を表示するものです。

search

  @objs = Class->search($key, $value);
  @objs = $obj->search($key, $value);

格納されているオブジェクトについて、$key が $value の値をもつオブジェクトをすべて返す、単純な検索です。
    @films = Film->search('Rating', 'PG');

search_like

  @objs = Class->search_like($key, $like_pattern);
  @objs = $obj->search_like($key, $like_pattern);

$key が $like_pattern にマッチするオブジェクトをさがす単純な検索です。$like_pattern は SQL の LIKE 述語で記述されたパターンです。'%' は "任意の1文字以上の単語", '_' は "任意の1文字の単語" を意味します。

XXX % と _ のかわりに、* と ? を使ったグロブスタイル版もつくるべき?

    # Bob という名前の人が監督した映画をさがす
    @films = Film->search_like('Director', 'Bob %');


うーん... まあ、概要があるし。

XXX もっと例が必要ですね。そのうち埋まるでしょう。


警告

単純なスカラ値しか格納できない

SQLってのは困ったもので、リストを格納するのは複雑で、ハッシュを格納するのは、1個テーブルが必要になります。 これ以上複雑なことはまだはじめないようにしてください。もしリストを格納したければ、 自分自身でアクセサを書いてください(これの処理方法はそのうち実験するつもりです)。 もしハッシュを格納したければ、もう1つテーブルとクラスを作ることを考慮した方がいいでしょう。

だれかに説得されて、自動的にデータをシリアライズするアクセサをつくるかもしれません。

1テーブル、1クラス

すべてのクラスに1つテーブルを定義して下さい。2個以上のテーブルにひとつのクラスが 分散することはできません。これを扱えるようにするのは、かなり頭が痛い話です。

最終的には、これらのテーブルのリンクや、リストのデータを表すテーブルに関する制限を外すことになるでしょう。

PRIMARY KEY のカラムは1個だけ

SQLテーブルに、2個以上 PRIMARY KEY を持たせるのは、現状サポートされていません。なぜって? 複雑だからです。将来のバージョンで、複数のキーをサポートします。


TODO

テーブル/オブジェクトの関係をハンドルすべき

2つのテーブル/オブジェクトの関係を扱ううまい方法がありません。 最終的には、とても単純な方法で、これらの関係をサポートしていくつもりです。

リストのサポートが貧弱

もののリストをオブジェクトデータとして扱ううまい方法がありません。 これもまた、最終的には実装しようと考えているもののひとつです。

疑似ハッシュをオブジェクトとして用いる方法をドキュメントにすべき

献立集を書く必要がある

オブジェクトのキャッシングを追加する必要がある

複数カラムの PRIMARY KEY についてテストしていない

もしこの機能が必要ならば、教えて下さい。動くようにします。

もっといろんなデータベースでテストすべき

複雑なデータを Storable で格納することが必要

並行処理の問題がある

rollback() は並行処理の問題がある

トランザクションの操作がもっと簡単にできるように

$obj->commit は DBI->commit にすべき???

クラスワイドなコミットとロールバックの、簡単な方法が必要。


バグと警告

テストした環境

DBD::mysql - MySQL 3.22 and 3.23
DBD::Pg - PostgreSQL 7.0
DBD::CSV

動作しないことがわかっている環境

DBD::RAM


AUTHOR

Michael G Schwern <schwern@pobox.com> Uri Gutman, Damian Conway, Mike Lambert そして POOP グループに、たくさん 深夜にわたっての助けをもらいました。


参考文献

Ima::DBI, Class::Accessor, base, Class::Data::Inheritable http://www.pobox.com/~schwern/papers/Class-DBI/, Perl Object-Oriented Persistence <poop-group@lists.sourceforge.net>, Alzaboそして Tangram