名前

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();

複雑なWHERE句

時には、特定の演算子を使用して問い合わせを明確に表さなければならないこともあるでしょう:

  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を参照してください。

カスタムの結果ソースを使用した任意のSQL

あまりにも複雑な問い合わせ(例えば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のショートカットです。columnsselect及びasと同時に使用することはできません。

データベース関数又はストアドプロシージャを使用する

selectasの組み合わせは、データベース関数又はストアドプロシージャの結果をカラムの値として取得するのに使用します。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');

複数のカラムでのSELECT DISTINCT

  my $rs = $schema->resultset('Foo')->search(
    {},
    {
      select => [
        { distinct => [ $source->columns ] }
      ],
      as => [ $source->columns ] # "as"がSQLのAS句と同じではないことに注意 :-)
    }
  );

  my $count = $rs->next->get_column('count');

SELECT COUNT(DISTINCT カラム名)

  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

上記の属性(joinselectas及び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の時点では、prefetchhas_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;

DBからきれいに切断する

開発の最中で、何度も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でどのようにしてバージョン化されたスキーマを顧客へ納品するかについて、かいつまんでご紹介します。基本的な手順は次のとおりです:

  1. DBIx::Classのスキーマを作成する

  2. スキーマを保存する

  3. 顧客へ納品する

  4. 仕様変更に合わせてスキーマを修正する

  5. 最新版を顧客へ納品する

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

顧客へ納品する

スキーマを納品する方法にはいくつかがあります。このレシピの範囲外になりますが、次のようにすると良いでしょう:

  1. RDBMSに手動でスキーマを適用するよう、顧客へ依頼する

  2. データベースのダンプ及びスキーマ、更新版及びテストスィートをインストール物件に含め、アプリケーションと共にパッケージ化する

仕様変更に合わせてスキーマを修正する

アプリケーションに改良が加えられた場合は、スキーマに対して仕様変更による修正を行う必要があるかもしれません。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でLimit句の方言を設定する

いくつかのケースでは、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文をクォートする

カラム名が空白及び(又は)予約語を含んでいる場合は、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つのカラム(idnumber及び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を使用してDBIx::Classをデバッグする

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プロキシクラスのサブクラスを動的に作成する(別名「1テーブルから複数クラスへのオブジェクトインフレーション」)

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_createメソッドが行を見つけたかどうか、又は作成したかどうかを知りたい

代わりに、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;
  }

原文へのリンク

DBIx::Class::Manual::Cookbook

翻訳者

Bahoo! <[email protected]> (翻訳:2007年8月12日)

これまでに翻訳したpod一覧