DBICで本当に困ったら SCALAR REFERNCE を使え

追記 2006/12/06

下記で IS NOT NULL を実現するのにスカラーリファレンスを使用していますが,IS NULL / IS NOT NULL を出すためには必ずしもスカラーリファレンスを利用する必要はありません。ということで訂正を入れようと思ったんですがちょっと長いので「フォローアップ記事」を書きました。

本題

typester さんに以前教えて頂いたんですが,似たようなことに今日遭遇したのでメモ。

WHERE field1 IS NOT NULL な検索をしようと思って,

$resultset->search({ field1 => 'IS NOT NULL' });

と書くと,内部的には

SELECT ... WHERE field1 = ?

と展開されて,プレースホルダに「IS NOT NULL」が渡されるので,バツ

$resultset->search({ field1 => { 'IS NOT' => 'NULL' } });

SELECT ... WHERE field1 IS NOT ?

と展開されるんですが,DBI 側で syntax error になります。
では,正解は?

$resultset->search({ field1 => \'IS NOT NULL' });

のようにスカラのリファレンスを渡すと,

SELECT ... WHERE field1 IS NOT NULL

のように直に展開されます。このように凝った条件を書くなら必要になってきますが,とうぜん,生でいじることになるので SQL インジェクション等には要注意です(今回の例ではまったくもって大丈夫ですが)。

おまけ

nekokak さんの Wiki は実例が多くて参考になります(Manual::Intro 読むより)。
そこに「might_haveとhas_oneの違いがよーわからんので調べる。」とあったので僭越ながら解説を(長らく編集されてないのでもうご存じかもと思いますが)。
has_one と might_have は,いずれも has_many のように外側のテーブルからリファレンスを貼るタイプのリレーションであり,前者は「外部テーブルのレコードをかならず持ってる」後者は「外部レコードを持ってるかもしれない」ものです。外部テーブルのレコードを普通に $user->credit_card みたいにアクセサでアクセスするだけでは違いは実感できませんが,prefetch をすると違いがよくわかります。

サンプルとして,

create table t_user (
  id int(10) not null,
  primary key (id),
);

create table t_credit_card (
  id      int(10) not null,
  user_id int(10) not null,
  holder  varchar,
  primary key (id),
  foreign key (user_id) references t_user(id),
);

こんなテーブル構造があったとします。

MyApp::Schema::User->has_one(credit_card => 'MyApp::Schema::CreditCard', 'user_id');

このようにリレーションを has_one で貼っているとすると(ユーザは必ずクレジットカードを持っている),

my $users = $schema->resultset('User')->search(
  {
    # every
  },
  {
    prefetch => [ 'credit_card' ],
  },
)->all;

このように prefetch したときにテーブルを結合して SELECT してくれます。
具体的には,

SELECT me.id, credit_card.id, credit_card.user_id, credit_card.holder
FROM t_user me
JOIN t_credit_card credit_card
ON ( credit_card.user_id = me.id )

このような SQL が吐かれます。
それに対して,might_have でリレーションを貼っている場合(ユーザはクレジットカードを持っているかもしれないし持っていないかもしれない),

MyApp::Schema::User->might_have(credit_card => 'MyApp::Schema::CreditCard', 'user_id');

以下のような SQL が吐かれます。

SELECT me.id, credit_card.id, credit_card.user_id, credit_card.holder
FROM t_user me
LEFT JOIN t_credit_card credit_card
ON ( credit_card.user_id = me.id )

ほとんど間違い探しの世界ですが,後者では JOIN に「LEFT」という修飾子が付いています。逆に前者はないので,

  • has_one は INNER JOIN(t_user と t_credit_card の両者に存在するレコードのみ結合)
  • might_have は LEFT OUTER JOIN(t_credit_card に該当するレコードが存在すれば結合。存在しないときは null と結合)。

ということになります。


このように,prefetch は JOIN を使って実装しているので(少なくともいまのところ),

my $users = $schema->resultset('User')->search(
  {
    # every
  },
  {
    prefetch => [ 'credit_card', 'books' ],
  },
)->all;

のように複数のリレーションに対して並列に prefetch を掛けることはできません。即座にエラーがでるわけではないのでちょっとハマりぎみなポイントです。できなかないんですけどなんか挙動不審だったりします。

follow-up - [id:dayflower:20060804:1154653058] を書きました。

ちなみに並列じゃなくて,

my $users = $schema->resultset('User')->search(
  {
    # every
  },
  {
    prefetch => {
      'books' => {
        'authors' => 'contact_info',
      },
    },
  },
)->all;

一方向にたどっていくリレーションであれば一度に JOIN してくれます。