DBIx::Class での JOIN

DBIx::Class::Manul::Cookbook の "Using joins and prefetch" の前半の抄訳です。仕事上必要になってラフに読んだので参考程度に。

リレーション対象のテーブルの1つ以上のカラムを取得したり,それでソートしたりするには,join アトリビュートを使います。あるアーティスト名にマッチするすべての CD を得るには以下のようにします:

my $rs = $schema->resultset('CD')->search(
  {
    'artist.name' => 'Bob Marley'    
  },
  {
    join => [qw/artist/], # join the artist table
  }
);

# 以下の 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 テーブルとアーティストテーブルを探索しましたが,データ自体は CD テーブル からしか取得していないのです。取得した CD オブジェクトに対応するアーティスト名を得るために,DBIx::Class はふたたびデータベースを検索しなくてはなりません:

SELECT artist.* FROM artist WHERE artist.id = ?

主クエリで返される CD オブジェクトすべてについて,それぞれ上記のような SQL が実行されるのです。5 つの CD にたいしては 5 つのクエリが実行されます。100 枚の CD に対しては 100 ものクエリが追加実行されるのです!

ありがたいことに,この問題を解決するための prefetch アトリビュートというものが DBIx::Class に含まれています。このアトリビュートを用いるとリレーション先のテーブルの結果をあらかじめ取得することができます:

my $rs = $schema->resultset('CD')->search(
  {
    'artist.name' => 'Bob Marley'
  },
  {
    join     => [qw/ artist /],
    order_by => [qw/ artist.name /],
    prefetch => [qw/ artist /] # return artist data too!
  }
);

# 以下の 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 はあらかじめアーティストテーブルから合致するデータを読み込みます。ですから追加実行される SQL 文はありません。改変前より,とても効率的なクエリが発行されます。

DBIx::Class 0.05999_01 以降より,prefetchhas_many リレーションと組み合わせて使えるようになりました。

prefetch はリレーション先のテーブルのデータを確実に使う予定があるときだけ使用してください。メインテーブルの結果だけ必要なのに,リレーション先のテーブルをプリフェッチするとやっぱりパフォーマンスは悪化しますよ!