Catalystでnamed_scope風実装

泣きながらPerl/Catalystを書いている今日この頃です。慣れない言語は辛い。

PerlのORマッパーであるDBIx::Classを使ってるんですが、とりあえずDBIx::Class beginnersを見ると、使い方の指針やFat Modelっぽい書き方が載っていて大変良いです。ActiveRecord大好きです。

で、CatalystにはRailsのnamed_scopeのような便利な記法は標準では提供されていません。多分。でもDBIx::Class::ResultSetのサブクラスを使うようにすれば、それっぽいことはできるようになります。

まず

アプリ名をMyAppとします。

my $rs = $c->("DBIC::Foo")が返すのはDBIx::Class::ResultSetのインスタンスで、これはクエリを投げて返ってきた集合に対して処理をするために用います。

一方、例えばmy $foo = $rs->firstが返すのはMyApp::Schema::Result::Fooのインスタンスで、これは個々のレコードに対して処理をするために用います。

なので、RailsのActiveRecordの雰囲気的にはDBIx::Class::ResultSetにモデルのクラスメソッド・named_scopeを実装して、MyApp::Schema::Result::Fooにモデルのインスタンスメソッドを実装する、という感じになります。

::Result::Fooに::ResultSet::Fooを対応付ける

デフォルトだとResultSetにはDBIx::Class::ResultSetが使われるため、全てのテーブルで共通の実装をすることになってしまい、あまり嬉しくありません(共通の処理を実装するなら別ですが)。

なのでまずはMyApp::Schema::Result::FooにMyApp::Schema::ResultSet::Fooを対応付ける必要があります。

この機能は各::Result::*で共通で利用したいので、lib/MyApp/Schema/Result/Base.pm を継承して使えるようにしておきます。

package MyApp::Schema::Result::Base;
use strict;
use warnings;

# 継承されたときに呼ばれる、らしい
sub import {
  my $caller = caller; # 継承の子のパッケージ

  # 継承の子側に対して、resultset_classに::ResultSet::Fooを設定する。
  # クラス名はResultをResultSetに置換したもの。
  $caller->resultset_class(sub {
    my $pkg = $caller;
    $pkg =~ s/Result/ResultSet/;
    return $pkg;
  }->());
}

1;

さて、lib/MyApp/Schema/Result/Foo.pm ではBase.pmを継承するだけです。

package MyApp::Schema::Result::Foo;
use strict;
use warnings;

use MyApp::Schema::Result::Base;

1;

以上で MyApp::Schema::ResultSet::Foo があれば$c->model("DBIC::Foo")はそのインスタンスを返し、無ければこれまでどおりDBIx::Class::ResulSetのインスタンスを返すようになります。

なお、MyApp::Schema::ResultSet::FooはDBIx::Class::ResulSetを継承する必要があります。

package MyApp::Schema::ResultSet::Foo;
use base 'DBIx::Class::ResulSet';
1;

named_scope風のものを::ResultSet::Fooに実装する

package MyApp::Schema::ResultSet::Foo;
use base 'DBIx::Class::ResulSet';

sub recent {
  my ($self, $c) = @_;
  return $self->search({}, { order_by => { -desc => [qw/created_at/] }, rows => 10 });
}

sub public {
  my ($self, $c) = @_;
  return $self->search({ is_public => 1 });
}

1;

searchはまた::ResultSet::*を返すので、チェインすることでさらに絞り込めます。

my $foos = $c->model("DBIC::Foo")->recent->public;

遅延評価されるため、SQLの発行も最後の1回だけです。named_scopeっぽい。