DBIx::Class::Manual::Cookbook - 様々なレシピ
問い合わせに対して大量の結果が返されることが予想される時は、DBIx::Classに対して結果セットをページ分け(一度に少しずつ取得)するよう要求することができます:
my $rs = $schema->resultset('Artist')->search( undef, { page => 1, # 取得したいページ番号(デフォルトは1) rows => 10, # ページ毎の件数 }, ); return $rs->all(); # 1ページ目を全て取得する
page
属性は、検索において必ずしも指定する必要はありません:
my $rs = $schema->resultset('Artist')->search( undef, { rows => 10, } ); return $rs->page(1); # 最初の10レコードを含むDBIx::Class::ResultSet
上記のいずれのケースにおいても、pager
メソッドを使用することで、結果セットに対するData::Pageオブジェクト(テンプレート等での使用に適する)を取得することができます:
return $rs->pager();
時には、特定の演算子を使用して問い合わせを明確に表さなければならないこともあるでしょう:
my @albums = $schema->resultset('Album')->search({ artist => { 'like', '%Lamb%' }, title => { 'like', '%Fear of Fours%' }, });
これは結局、次のWHERE
句のようになります:
WHERE artist LIKE '%Lamb%' AND title LIKE '%Fear of Fours%'
別の問い合わせでは、若干複雑なロジックが必要となるかもしれません:
my @albums = $schema->resultset('Album')->search({ -or => [ -and => [ artist => { 'like', '%Smashing Pumpkins%' }, title => 'Siamese Dream', ], artist => 'Starchildren', ], });
これは、最終的には次のWHERE
句になります:
WHERE ( artist LIKE '%Smashing Pumpkins%' AND title = 'Siamese Dream' ) OR artist = 'Starchildren'
複雑な問い合わせを作成するための更なる情報については、SQL::Abstract::WHERE_CLAUSESを参照してください。
あまりにも複雑な問い合わせ(例えばUnionや副問い合わせ、ストアドプロシージャなどを含む)を行う場合や、特別な方法でデータベース問い合わせの最適化を行う場合には、任意のSQLを実行しなければならないかもしれません。しかし、その場合でも、結果セットをDBIx::Class::ResultSetのオブジェクトとして取得したいかもしれません。その場合にお勧めする方法は、問い合わせのための結果ソースを個別に定義することです。そして、スカラーリファレンスを使用して完全なSQL文を挿入することができます(これは、SQL::Abstractの機能です)。
例えば、userのデータに対して複雑なカスタムクエリを実行したいとして、Userクラスに追加しなければならないものは以下のとおりです:
package My::Schema::User; use base qw/DBIx::Class/; # ->load_components, ->table, ->add_columns, など # Userクラスをベースにした新たな結果ソースを作成する my $source = __PACKAGE__->result_source_instance(); my $new_source = $source->new( $source ); $new_source->source_name( 'UserFriendsComplex' ); # 問い合わせをスカラーリファレンスとして渡す # これは、FROMの後の副問い合わせとして追加される # したがって、全体をかっこで囲む必要があることに注意する $new_source->name( \<<SQL ); ( SELECT u.* FROM user u INNER JOIN user_friends f ON u.id = f.user_id WHERE f.friend_user_id = ? UNION SELECT u.* FROM user u INNER JOIN user_friends f ON u.id = f.friend_user_id WHERE f.user_id = ? ) SQL # 最後に、新しい結果ソースをスキーマに登録する My::Schema->register_source( 'UserFriendsComplex' => $new_source );
そして、以下のようにパラメータをバインドすることで、複雑な問い合わせを実行することができます:
my $friends = [ $schema->resultset( 'UserFriendsComplex' )->search( {}, { bind => [ 12345, 12345 ] } ) ];
... これで、完全なDBIx::Class::ResultSetオブジェクトを取得することができます。
テーブルから特定のカラムのみを取得したい場合は、columns
属性を使用して、どのカラムが必要なのかを特定することができます。これは、どのみちすぐに必要とはしない大量のデータを含むカラムをロードしてしまうのを防ぐのに役立ちます。
my $rs = $schema->resultset('Artist')->search( undef, { columns => [qw/ name /] } ); # 等価なSQL: # SELECT artist.name FROM artist
これは後述にもあるように、select
及びas
のショートカットです。columns
はselect
及びas
と同時に使用することはできません。
select
とas
の組み合わせは、データベース関数又はストアドプロシージャの結果をカラムの値として取得するのに使用します。select
で、カラムの値の元(例:カラム名、関数名又はストアドプロシージャ名)を指定し、as
で返却値にアクセスする際のカラム名を指定します:
my $rs = $schema->resultset('Artist')->search( {}, { select => [ 'name', { LENGTH => 'name' } ], as => [qw/ name name_length /], } ); # 等価なSQL: # SELECT name name, LENGTH( name ) # FROM artist
なお、as
属性がSQL文のSELECT foo AS bar
とは全く無関係であることに留意してください(詳細はDBIx::Class::ResultSet::ATTRIBUTESをご覧ください)。ベースクラスの実在するカラム名(add_columns
メソッドによって追加された等)と同名のエイリアスをas
に設定した場合は、通常どおりアクセスしてください。例えば、Artist
クラスはname
カラムを持っているので、ここでは単にname
アクセサを使用します:
my $artist = $rs->first(); my $name = $artist->name();
一方で、as
に設定したエイリアス名が実在するカラム名に一致しない場合は、get_column
アクセサを使用して値を取得する必要があります:
my $name_length = $artist->get_column('name_length');
get_column
アクセサを使いたくない、という場合は、次のいずれかを使用すれば、お好みのアクセサを作成することができます:
# アクセサを手動で定義する: sub name_length { shift->get_column('name_length'); } # 又は、DBIx::Class::AccessorGroupを使用する: __PACKAGE__->mk_group_accessors('column' => 'name_length');
my $rs = $schema->resultset('Foo')->search( {}, { select => [ { distinct => [ $source->columns ] } ], as => [ $source->columns ] # "as"がSQLのAS句と同じではないことに注意 :-) } ); my $count = $rs->next->get_column('count');
my $rs = $schema->resultset('Foo')->search( {}, { select => [ { count => { distinct => 'colname' } } ], as => [ 'count' ] } );
DBIx::Classでは、次のとおりGROUP BY
をサポートしています:
my $rs = $schema->resultset('Artist')->search( {}, { join => [qw/ cds /], select => [ 'name', { count => 'cds.cdid' } ], as => [qw/ name cd_count /], group_by => [qw/ name /] } ); # 等価なSQL: # SELECT name, COUNT( cds.cdid ) FROM artist me # LEFT JOIN cd cds ON ( cds.artist = me.artistid ) # GROUP BY name
上記の属性(join
、select
、as
及びgroup_by
)の使い方がどうしても分からない、という場合は、DBIx::Class::ResultSet::ATTRIBUTESを参照してください。
よく使う検索については、DBIx::Class::ResultSetを継承したクラスに、独自のメソッドとして定義することができます:
package My::DBIC::ResultSet::CD; use strict; use warnings; use base 'DBIx::Class::ResultSet'; sub search_cds_ordered { my ($self) = @_; return $self->search( {}, { order_by => 'name DESC' }, ); } 1;
作成した結果セットを使用する場合は、My::DBIC::Schema::CDクラスの中で、結果セットのインスタンスを作成することをDBIx::Classに伝える必要があります:
__PACKAGE__->resultset_class('My::DBIC::ResultSet::CD');
その上で、コードの中で新しいメソッドを呼び出します:
my $ordered_cds = $schema->resultset('CD')->search_cds_ordered();
関連テーブルの1つ以上のカラムについて検索したり、結果をソートするために、join
属性を使用することができます。特定のアーティスト名にマッチする全てのCDを取得する場合は、次のようにします:
my $rs = $schema->resultset('CD')->search( { 'artist.name' => 'Bob Marley' }, { join => [qw/artist/], # artistテーブルを結合する } ); # 等価なSQL: # SELECT cd.* FROM cd # JOIN artist ON cd.artist = artist.id # WHERE artist.name = 'Bob Marley'
必要なら、order_by
属性を含めることで、関連テーブルのカラムでソートすることもできます:
my $rs = $schema->resultset('CD')->search( { 'artist.name' => 'Bob Marley' }, { join => [qw/ artist /], order_by => [qw/ artist.name /] } }; # 等価なSQL: # SELECT cd.* FROM cd # JOIN artist ON cd.artist = artist.id # WHERE artist.name = 'Bob Marley' # ORDER BY artist.name
join
属性は、関連テーブルのカラムを検索したりソートしたりする場合にのみ使用すべきである、ということに留意してください。主テーブルのカラムしか必要でないのに、関連テーブルを結合するのは、パフォーマンスの低下につながります!
さて、アーティストの名前付きでCDのリストを表示したいとします。次のコードはうまく機能するでしょう:
while (my $cd = $rs->next) { print "CD: " . $cd->title . ", Artist: " . $cd->artist->name; }
ただし、問題を含んでいます。メインの問い合わせでは、cd
テーブルとartist
テーブルの両方に対して検索を行っていますが、cd
テーブルのデータしか返ってきません。取得したCDに関連するアーティスト名を取得するため、DBIx::Classはデータベースに対して、次のような問い合わせを行います:
SELECT artist.* FROM artist WHERE artist.id = ?
上記のようなSQL文が、メインの問い合わせでCDを取得する度に実行されます。CDが5枚なら、5回の余計な問い合わせが発生します。CDが100枚なら、100回もの余計な問い合わせが発生するのです!
ありがたい事に、DBIx::Classはこの問題を解決するためのprefetch
という属性を持っています。これで、関連テーブルから前もって結果を取得することができます:
my $rs = $schema->resultset('CD')->search( { 'artist.name' => 'Bob Marley' }, { join => [qw/ artist /], order_by => [qw/ artist.name /], prefetch => [qw/ artist /] # アーティストのデータも併せて返します! } ); # 等価なSQL("cd"と"artist"の両方に対してSELECTすることに留意): # SELECT cd.*, artist.* FROM cd # JOIN artist ON cd.artist = artist.id # WHERE artist.name = 'Bob Marley' # ORDER BY artist.name
CDのリストを表示するコードはそのままです:
while (my $cd = $rs->next) { print "CD: " . $cd->title . ", Artist: " . $cd->artist->name; }
DBIx::Classは、artist
テーブルからマッチした全てのデータをプリフェッチするので、それ以上余計なSQL文は実行されません。これで、以前よりもずっと効率的な問い合わせになりました。
DBIx::Classのバージョン0.05999_01の時点では、prefetch
はhas_many
のリレーションシップとともに使用する必要があることに留意してください。
なお、関連テーブルのデータを確実に使用すると分かっている場合に限り、prefetch
を使用するようにしてください。主テーブルのデータしか必要でないのに、関連テーブルからプリフェッチするのは、パフォーマンスの低下につながります!
時折、1階層よりも深い階層の結合を行いたいことがあります。次の例では、LinerNotes
に特定の文字列を含むCD
を持つ全てのArtist
オブジェクトを検索しています:
# 至る所に定義されたリレーションシップ: # Artist->has_many('cds' => 'CD', 'artist'); # CD->has_one('liner_notes' => 'LinerNotes', 'cd'); my $rs = $schema->resultset('Artist')->search( { 'liner_notes.notes' => { 'like', '%some text%' }, }, { join => { 'cds' => 'liner_notes' } } ); # 等価なSQL: # SELECT artist.* FROM artist # JOIN ( cd ON artist.id = cd.artist ) # JOIN ( liner_notes ON cd.id = liner_notes.cd ) # WHERE liner_notes.notes LIKE '%some text%'
結合は任意の階層までネストすることができます。上の例から、取得されるアーティストの数をライナーノーツの著者名で絞り込みたくなった場合は、次のようにします:
# 至る所に定義されたリレーションシップ: # LinerNotes->belongs_to('author' => 'Person'); my $rs = $schema->resultset('Artist')->search( { 'liner_notes.notes' => { 'like', '%some text%' }, 'author.name' => 'A. Writer' }, { join => { 'cds' => { 'liner_notes' => 'author' } } } ); # 等価なSQL: # SELECT artist.* FROM artist # JOIN ( cd ON artist.id = cd.artist ) # JOIN ( liner_notes ON cd.id = liner_notes.cd ) # JOIN ( author ON author.id = liner_notes.author ) # WHERE liner_notes.notes LIKE '%some text%' # AND author.name = 'A. Writer'
バージョン0.04999_05を超えてからは、マルチステップの結合と同じ文法で、1階層よりも深い階層でのprefetch
が行えるようになりました:
my $rs = $schema->resultset('Tag')->search( {}, { prefetch => { cd => 'artist' } } ); # 等価なSQL: # SELECT tag.*, cd.*, artist.* FROM tag # JOIN cd ON tag.cd = cd.cdid # JOIN artist ON cd.artist = artist.artistid
これにより、cd
及びartist
へのアクセスに余計なSQL文が実行されなくなります:
my $tag = $rs->first; print $tag->cd->artist->name;
特定のカラムの合計を取得したい場合は、いくつか方法があります。検索を使えば一目瞭然です:
my $rs = $schema->resultset('Items')->search( {}, { select => [ { sum => 'Cost' } ], as => [ 'total_cost' ], # 'as'はDBIx::Class::ResultSetであり、SQLではないことに注意 } ); my $tc = $rs->first->get_column('total_cost');
もしくは、DBIx::Class::ResultSetColumnを使用します。ResultSet
からget_column
でカラムを取得した後、次のようにしたり:
my $cost = $schema->resultset('Items')->get_column('Cost'); my $tc = $cost->sum;
こんなこともできます:
my $minvalue = $cost->min; my $maxvalue = $cost->max;
または、このカラムのみをイテレートすることもできます:
while ( my $c = $cost->next ) { print $c; } foreach my $c ($cost->all) { print $c; }
ResultSetColumn
だけが、上記のような組み込み関数を(わずかですが)持っています。しかし、その中にない他の関数を使用したい場合は、代わりにfunc
メソッドを使用することができます:
my $avg = $cost->func('AVERAGE');
これは、次のSQLを実行します:
SELECT AVERAGE(Cost) FROM Items me
これは、データベースがその関数をサポートしていなければ当然機能しません。詳細については、DBIx::Class::ResultSetColumnを参照してください。
my $book->create_related('author', { name => 'Fred'});
"Titanic"という名前の本の著者のみを$authorから検索します。
my $author->search_related('books', { name => 'Titanic' });
"Titanic"という名前の本の著者のみを$authorから削除します。
my $author->delete_related('books', { name => 'Titanic' });
リレーションを常にソートしたい場合は、リレーションシップ作成時にそれを指定することができます。
$book->pages
をpage_numberの降順でソートする場合は、次のようにします。
Book->has_many('pages' => 'Page', 'book', { order_by => \'page_number DESC'} );
バージョン0.04001以降、DBIx::Class::Storage及びDBIx::Class::Schemaは改良されたトランザクションをサポートしています。以下は、トランザクションを使用する場合のお勧めの方法です:
my $genus = $schema->resultset('Genus')->find(12); my $coderef2 = sub { $genus->extinct(1); $genus->update; }; my $coderef1 = sub { $genus->add_to_species({ name => 'troglodyte' }); $genus->wings(2); $genus->update; $schema->txn_do($coderef2); # トランザクションの入れ子も可能 return $genus->species; }; my $rs; eval { $rs = $schema->txn_do($coderef1); }; if ($@) { # トランザクション失敗 die "the sky is falling!" # if ($@ =~ /Rollback failed/); # ロールバック失敗 deal_with_failed_transaction(); }
入れ子のトランザクションは、期待どおりの動作をします。つまり、実際には最も外側にあるトランザクションだけが$dbhにコミットを発行し、どの階層のトランザクションでロールバックが起きた場合でも、入れ子のトランザクション全体の失敗になります。セーブポイント及び本物の入れ子のトランザクション(それらをサポートするデータベース向けに)機能については、できれば将来サポートしたいと考えています。
次は、多対多のストレートな例です:
package My::DB; # 接続をセットアップする package My::User; use base 'My::DB'; __PACKAGE__->table('user'); __PACKAGE__->add_columns(qw/id name/); __PACKAGE__->set_primary_key('id'); __PACKAGE__->has_many('user_address' => 'My::UserAddress', 'user'); __PACKAGE__->many_to_many('addresses' => 'user_address', 'address'); package My::UserAddress; use base 'My::DB'; __PACKAGE__->table('user_address'); __PACKAGE__->add_columns(qw/user address/); __PACKAGE__->set_primary_key(qw/user address/); __PACKAGE__->belongs_to('user' => 'My::User'); __PACKAGE__->belongs_to('address' => 'My::Address'); package My::Address; use base 'My::DB'; __PACKAGE__->table('address'); __PACKAGE__->add_columns(qw/id street town area_code country/); __PACKAGE__->set_primary_key('id'); __PACKAGE__->has_many('user_address' => 'My::UserAddress', 'address'); __PACKAGE__->many_to_many('users' => 'user_address', 'user'); $rs = $user->addresses(); # そのユーザの全アドレスを取得する $rs = $address->users(); # そのアドレスを持つ全ユーザを取得する
new
メソッドをオーバーライドするだけの簡単さです。ただし、next::method
を使用する必要があることに注意してください。
sub new { my ( $class, $attrs ) = @_; $attrs->{foo} = 'bar' unless defined $attrs->{foo}; my $new = $class->next::method($attrs); return $new; }
next::method
についての詳細は、Class::C3のドキュメンテーションを参照してください。独自のベースクラスを書くためのさらなる情報については、DBIx::Class::Manual::Componentをご覧ください。
DBIx::Classで“トリガー”を使用する方法を探している人は、おそらくここを見つけることでしょう。
overload
モジュールによって、標準的な文字列化のテクニックを使用します。
オブジェクトを単一のカラムとして文字列化するには、次のようにします(foo
は、選択したカラム又はメソッドで置き換えます):
use overload '""' => sub { shift->name}, fallback => 1;
やや複雑な文字列化として、無名サブルーチンを使用することもできます:
use overload '""' => sub { $_[0]->name . ", " . $_[0]->address }, fallback => 1;
Product
及びCategory
の2つのテーブルがあるとします。テーブルの仕様は以下のとおりです:
Product(id, Description, category) Category(id, Description)
ここで、category
はCategoryテーブルを参照する外部キーです。
Productテーブルのオブジェクト$objがあり、次のように書いた場合は、
print $obj->category
期待どおりには動作しないでしょう。
例えば、カテゴリーのdescriptionを取得したい場合は、Categoryテーブルを定義するクラスに、以下を追加するとよいでしょう:
use overload "" => sub { my $self = shift; return $self->Description; }, fallback => 1;
開発の最中で、何度もCtrl+Cでアプリケーションを終了することに気づいた場合は、DBからきれいに切断することを確実なものにするため、次のシグナルハンドラをメインのデータベースクラスに配置するとよいでしょう:
$SIG{INT} = sub { __PACKAGE__->storage->disconnect; };
この機能を使用するためには、SQL::Translator(別名“SQLの妖精”)をインストールしている必要があります。
既存のデータベースからDBIx::Classスキーマを作成するためには、以下のようにします:
sqlt --from DBI --to DBIx::Class::File --prefix "MySchema" > MySchema.pm
既存のDBIx::ClassスキーマからMySQLデータベースを作成する場合は、スキーマをMySQLの方言に変換します:
sqlt --from SQL::Translator::Parser::DBIx::Class --to MySQL --DBIx::Class "MySchema.pm" > Schema1.sql
そして、mysqlクライアントを使用してインポートします:
mysql -h "host" -D "database" -u "user" -p < Schema1.sql
DBIx::Classにスキーマベースでアプローチしたいけれど、クラスベースのセットアップで既に多数の既存のクラスを使用しており、手作業で移行したくない場合には、代わりにこの気の利いたスクリプトを使用するとよいでしょう:
use MyDB; use SQL::Translator; my $schema = MyDB->schema_instance; my $translator = SQL::Translator->new( debug => $debug || 0, trace => $trace || 0, no_comments => $no_comments || 0, show_warnings => $show_warnings || 0, add_drop_table => $add_drop_table || 0, validate => $validate || 0, parser_args => { 'DBIx::Schema' => $schema, }, producer_args => { 'prefix' => 'My::Schema', }, ); $translator->parser('SQL::Translator::Parser::DBIx::Class'); $translator->producer('SQL::Translator::Producer::DBIx::Class::File'); my $output = $translator->translate(@args) or die "Error: " . $translator->error; print $output;
MyDB::*名前空間の全てのサブクラスを探し出すには、Module::Findを使用すると良いでしょう。方法は読者への課題に残しておきます。
次の例は、DBIx::Classでどのようにしてバージョン化されたスキーマを顧客へ納品するかについて、かいつまんでご紹介します。基本的な手順は次のとおりです:
DBIx::Classのスキーマを作成する
スキーマを保存する
顧客へ納品する
仕様変更に合わせてスキーマを修正する
最新版を顧客へ納品する
これは手動で行うか、又はスキーマのインポート及びエクスポートの項で説明したように、既存のデータベースから生成することができます。
sqlt
を使用して、スキーマを顧客のデータベースに合ったSQLスクリプトに変換します。例えば、MySQLでは次のようになります:
sqlt --from SQL::Translator::Parser::DBIx::Class --to MySQL --DBIx::Class "MySchema.pm" > Schema1.mysql.sql
複数のベンダのデータベースに合わせる必要がある場合は、単に、個別のデータベースに合わせてSQLスクリプトを生成します。PostgreSQLをサポートする場合は次のようにします:
sqlt --from SQL::Translator::DBIx::Class --to PostgreSQL --DBIx::Class "MySchema.pm" > Schema1.pgsql.sql
スキーマを納品する方法にはいくつかがあります。このレシピの範囲外になりますが、次のようにすると良いでしょう:
RDBMSに手動でスキーマを適用するよう、顧客へ依頼する
データベースのダンプ及びスキーマ、更新版及びテストスィートをインストール物件に含め、アプリケーションと共にパッケージ化する
アプリケーションに改良が加えられた場合は、スキーマに対して仕様変更による修正を行う必要があるかもしれません。DBIx::Classのスキーマに変更が加えられた場合は、オリジナルを上書きしないよう注意しながら、前回と同様に修正後のスキーマをエクスポートします:
sqlt --from SQL::Translator::DBIx::Class --to MySQL --DBIx::Class "Anything.pm" > Schema2.mysql.sql
次に、顧客のデータベーススキーマを更新するためのSQLスクリプトを、sqlt-diff
を使用して作成します:
sqlt-diff --to MySQL Schema1=MySQL Schema2=MySQL > SchemaUpdate.mysql.sql
最新版のスキーマは、前回と同様の方法で顧客へ納品することができます。
いくつかのケースでは、SQL::Abstract::Limitが、データベースハンドルを見てもリモートのSQLサーバの方言を特定することができない場合があります。これは、DBD::JDBCを使用する場合によくある問題です。というのも、DBDドライバは、Javaドライバが利用可能かどうかのみを知ることができ、JavaコンポーネントがどのJDBCドライバをロードしたかを知ることができないからです。次の例は、Microsoft SQL-Serverに対してlimit_dialectを設定します(その他の例については、SQL::Abtract::Limitのドキュメンテーションを参照してください)。
__PACKAGE__->storage->sql_maker->limit_dialect('mssql');
なお、JDBCブリッジは、Mircosoftがネイティブライブラリを提供していないプラットフォーム(例えばLinuxなど)からMSSQLサーバへアクセスする場合の一手段です。
カラム名が空白及び(又は)予約語を含んでいる場合は、SQL文の発行の際にクォートする必要があります。これは、次のようにします:
__PACKAGE__->storage->sql_maker->quote_char([ qw/[ ]/] ); __PACKAGE__->storage->sql_maker->name_sep('.');
まず、クォートに使用する文字を設定します。これには、一対のブラケット、もしくは1つのダブルクォート(")又はシングルクォート(')を指定します:
__PACKAGE__->storage->sql_maker->quote_char('"');
指定すべきクォート文字の種類については、データベースのドキュメンテーションを確認してください。なお、SQLジェネレータが適切な場所をクォートできるよう、name_sep
を設定する必要があります。
DBIx::Classは、メソッド呼び出しの再ディスパッチにClass::C3を使用しています。メソッドのオーバーライドを行うためには、next::method
を呼び出す必要があります。DBIx::ClassとともにClass::C3を使用する方法についての詳細は、DBIx::Class::Manual::Componentを参照してください。
例えば、3つのカラム(id
、number
及びsquared
)があるとします。そして、number
カラムを更新した時に、自動的にsquared
カラムにnumber
の二乗の値をセットしたいとします。これを実現するため、store_column
メソッドを次のようにオーバーライドすることができます:
sub store_column { my ( $self, $name, $value ) = @_; if ($name eq 'number') { $self->squared($value * $value); } $self->next::method($name, $value); }
next::method
の呼び出しにより、スーパークラスのstore_column
メソッド呼び出しが再ディスパッチされることに留意してください。
多数のCD
を持つArtist
クラスがあり、新たなArtist
オブジェクトを挿入した都度新たなCD
オブジェクトを作成したい場合は、insert
メソッドをオーバーライドします:
sub insert { my ( $self, @args ) = @_; $self->next::method(@args); $self->cds->new({})->fill_from_artist($self)->insert; return $self; }
fill_from_artistメソッドをCDクラスに記述します。これは、引数に渡したArtist
オブジェクトのデータを元に、CD
のデータを設定します。
Data::Dumperはデバッグに非常に有効なツールですが、時折、生成されたデータから該当する部分を見つけ出すのにとても苦労することがあります。特に、次のような場合、
use Data::Dumper; my $cd = $schema->resultset('CD')->find(1); print Dumper($cd);
CDオブジェクトのスキーマや結果ソースのデータが数ページにも渡って画面にダンプされます。通常は2~3のカラムのデータしか必要ないため、これではあまり役に立ちません。
しかし幸運にも、Data::Dumperが画面に出力する前に、内容を修正することができます。単に、Data::Dumperがデータをダンプする前にオブジェクトを呼び出すようなフックを定義してください。例えば、
package My::DB::CD; sub _dumper_hook { $_[0] = bless { %{ $_[0] }, result_source => undef, }, ref($_[0]); } [...] use Data::Dumper; local $Data::Dumper::Freezer = '_dumper_hook'; my $cd = $schema->resultset('CD')->find(1); print Dumper($cd); # 結果ソースなしで$cdをダンプする
スキーマが、全てのテーブルクラスに対する共通のベースクラスを持つ構造である場合は、そのベースクラスに_dumper_hook
と同様のメソッドを記述し、そのメソッド名を$Data::Dumper::Freezer
に設定します。これによりData::Dumperは、表示の直前にデータを自動的にクリーンアップします。詳細については、Data::Dumper::EXAMPLESを参照してください。
次のように、行オブジェクトからスキーマオブジェクトを取得することができます:
my $schema = $cd->result_source->schema; # スキーマを通常どおり使用する: my $artist_rs = $schema->resultset('Artist');
これは、一部のメソッドに対してスキーマオブジェクトを渡したくない場合に役に立ちます。
DBIx::Class::Storageのデバッギングを有効にした場合、実行したSQL文やバインドパラメータに補完した値、トランザクションの開始及びコミットに関する情報が表示されます。SQL文の詳細なプロファイリングを行いたい場合は、DBIx::Class::Storage::Statisticsのサブクラスを作成し、独自のプロファイリングの仕組みを書くことができます:
package My::Profiler; use strict; use base 'DBIx::Class::Storage::Statistics'; use Time::HiRes qw(time); my $start; sub query_start { my $self = shift(); my $sql = shift(); my $params = @_; print "Executing $sql: ".join(', ', @params)."\n"; $start = time(); } sub query_end { my $self = shift(); my $sql = shift(); my @params = @_; printf("Execution took %0.4f seconds.\n", time() - $start); $start = undef; } 1;
そして、このクラスをデバッギングオブジェクトとしてインストールします:
__PACKAGE__->storage()->debugobj(new My::Profiler()); __PACKAGE__->storage()->debug(1);
より複雑な例として、実行された全てのSQL文を1つの配列に格納します:
sub query_end { my $self = shift(); my $sql = shift(); my @params = @_; my $elapsed = time() - $start; push(@{ $calls{$sql} }, { params => \@params, elapsed => $elapsed }); }
SQL文の実行時間の平均、最大及び最小を調べることができ、特定のパラメータが異常な動作を引き起こしていないかどうか、深く探ることができます。
別名「last_insert_idの取得」
PK::Autoを使用している場合は、次の例がストレートな方法です:
my $foo = $rs->create(\%blah); # さらに別のことを行う my $id = $foo->id; # foo->my_primary_key_fieldでも機能する。
オートインクリメントの主キーを使用していない場合は、上記の例は恐らく機能しませんが、いずれにしても直前の主キーの値を取得できます。
DBIx::Classのクラスはプロキシクラスであるため、基本的なサブクラス作成法とは異なる、いくつかのテクニックが必要となります。次の例では、管理ユーザのための、ブーリアンのビットを格納する単一のテーブルを使用します。ここで、一般ユーザ向けのメソッドに加え、管理ユーザ向けの特別なメソッドを使用して、管理ユーザへオブジェクト(DBIx::Class::Row)を提供したいとします。この場合、2つの別々なプロキシクラスを作成するのは無意味です。ここでは、全てのユーザメソッドを管理ユーザのクラスへコピーします。以下は、上記を実現するための、よりクリーンな方法です。
一般ユーザのプロキシクラスに含まれるinflate_result
メソッドをオーバーライドすることで、期待どおりの効果が得られるでしょう。このメソッドは、結果をストレージからインフレートする際に、DBIx::Class::ResultSetから呼び出されます。そして、返却しようとしているオブジェクトを捕まえて値を検査し、それが管理ユーザのオブジェクトならブレスして返します。以下の例をご覧ください:
スキーマの定義
package DB::Schema; use base qw/DBIx::Class::Schema/; __PACKAGE__->load_classes(qw/User/);
プロキシクラスの定義
package DB::Schema::User; use strict; use warnings; use base qw/DBIx::Class/; ### ensure_class_loadedを呼び出すための管理ユーザクラスを定義する my $admin_class = __PACKAGE__ . '::Admin'; __PACKAGE__->load_components(qw/Core/); __PACKAGE__->table('users'); __PACKAGE__->add_columns(qw/user_id email password firstname lastname active admin/); __PACKAGE__->set_primary_key('user_id'); sub inflate_result { my $self = shift; my $ret = $self->next::method(@_); if( $ret->admin ) {### 管理ユーザクラスの場合、追加の機能のために再ブレスする $self->ensure_class_loaded( $admin_class ); bless $ret, $admin_class; } return $ret; } sub hello { print "I am a regular user.\n"; return ; } package DB::Schema::User::Admin; use strict; use warnings; use base qw/DB::Schema::User/; sub hello { print "I am an admin.\n"; return; } sub do_admin_stuff { print "I am doing admin stuff\n"; return ; }
テストファイル test.pl
use warnings; use strict; use DB::Schema; my $user_data = { email => '[email protected]', password => 'pass1', admin => 0 }; my $admin_data = { email => '[email protected]', password => 'pass2', admin => 1 }; my $schema = DB::Schema->connection('dbi:Pg:dbname=test'); $schema->resultset('User')->create( $user_data ); $schema->resultset('User')->create( $admin_data ); ### 検索する my $user = $schema->resultset('User')->single( $user_data ); my $admin = $schema->resultset('User')->single( $admin_data ); print ref $user, "\n"; print ref $admin, "\n"; print $user->password , "\n"; # パスワード1 print $admin->password , "\n";# パスワード2; Userを継承する print $user->hello , "\n"; # “I am a regular user.”と表示する print $admin->hello, "\n"; # “I am an admin.”と表示する ### 以下の文は表示されない print "I can do admin stuff\n" if $user->can('do_admin_stuff'); ### 以下の文は表示される print "I can do admin stuff\n" if $admin->can('do_admin_stuff');
DBIx::Classは処理速度を追求するようには作られておらず、利便性や扱いやすさを念頭において作られていますが、時折、手の込んだオブジェクトは飛ばして、単なるデータだけを取得したい場合があります。その場合は、単にDBIx::Class::ResultClass::HashRefInflatorを使用します。
my $rs = $schema->resultset('CD'); $rs->result_class('DBIx::Class::ResultClass::HashRefInflator'); my $hash_ref = $rs->find(1);
簡単ですよね?
上記のinflate_result
のソリューションで十分な処理速度が得られない場合でも、DBIx::Classでは、データベースから取得したそのままのデータを返すことができます。ただしこの場合、便利なメソッドでラッピングされなくなります。
これは、つぎのように使用します:‐
my $cursor = $rs->cursor while (my @vals = $cursor->next) { # ここで$val[0..n]を使用します }
この場合、配列のオフセットを特定のカラムにマッピングする必要があります(なお、select属性のsearch()
メソッドを使用することで、並び順を強制することができます)。
代わりに、find_or_new
を使用してください。その後、in_storage
をチェックします:
my $obj = $rs->find_or_new({ blah => 'blarg' }); unless ($obj->in_storage) { $obj->insert; # 行の作成の場合に行いたいことを記述します }
問題: “Camera”テーブルがあり、それぞれのカメラに説明を紐付けたい。ほとんどのカメラは、他のカラムから説明を生成することができるが、カスタムの説明をカメラに紐付けたい、という特別なケースがいくつかある。
ソリューション:
データベースのスキーマにて、“Camera”テーブルに、テキスト又はNULL値を含むことができるdescription(説明)フィールドを定義してください。
DBICでは、カスタムの説明が定義されていない場合に妥当なデフォルト値をセットするよう、カラムのアクセサをオーバーロードすることができます。アクセサは、フィールドがNULLかそうでないかによって、説明を生成するか又は返すかのいずれかを行います。
まず、“Camera”スキーマクラスで、次のようにdescription(説明)フィールドを定義します:
__PACKAGE__->add_columns(description => { accessor => '_description' });
次に、アクセサをラッピングするサブルーチンを定義します:
sub description { my $self = shift; # カラムが更新された場合は、オリジナルのアクセサに処理させる return $self->_description(@_) if @_; # カラムの値を取得する my $description = $self->_description; # descriptionフィールドに何らかの値が入っている場合は、それを返す return $description if defined $description && length $descripton; # そうでない場合は、説明を生成する return $self->generate_description; }
Bahoo! <[email protected]> (翻訳:2007年8月12日)