catalystでモデル分離が落ち着いた。その上でUser->find_by(id => 2)とかしてみた

いつまでモデル分離やってんだよって感じですが。
色々問題も解決していい感じに落ち着いた。

APIDBICCRUDなメソッドが呼べなかった件

  • 流れ
    • モデルを分離するためにAPIを作ろう
    • $c->model('API::User')でAPIが呼ばれるようにしよう
    • モデルを呼ぶのにwebのコンテキストから切り離せた!
    • でも当然DBICのsearchとかのメソッドはAPIから呼べないよね
    • searchとかしたい場合はresultsetオブジェクト用意してね
    • 毎回用意するの面倒です
  • 解決策

DBICCRUDなメソッドが使えるクラスを用意して、全APIがそれ継承すればいい


この考え自体は思いついてたし、pixisでもそうやってたから間違っては無いはず。
が、自分で実装ができなかったのと、pixis丸パクりは自分のためにならないのでやらなかったので、実現できなかった。


pixisのソースちゃんと読んだらやってること分かったのでさくっと実装。

  • 用意するクラスの必要条件
    • CRUDメソッドを持っている
    • テーブルの名前に依存しない


とりあえず、こうなる。

package AutoTest::API::Base::DBIC;
use strict;
use warnings;

use String::CamelCase qw/decamelize/;

sub new {
    my $class = shift;

    $class = ref $class || $class;
    bless { @_ }, $class;
}

sub class {
    my $self = shift;
    (split(/::/, ref $self))[-1];
}

sub resultset {
    my $self = shift;

    my $class = $self->class();
    my $schema = $AutoTest::Web::schema;

    return $schema->resultset($class);
}

### DBIC CRUD

sub create {
    my ($self, $args) = @_;
    my $rs = $self->resultset();
    $rs->create($args);
}

# different from the original
sub find {
    my ($self, $id) = @_;
    my $rs = $self->resultset();
    $rs->find($id);
}

sub search {
    my ($self, $cond, $attrs) = @_;

    my $rs = $self->resultset();
    $rs->search_rs($cond, $attrs);
}

sub update {
    my ($self, $args) = @_;
    my $rs = $self->resultset();
    $rs->create($args);
}

sub delete {
    my $self = shift;
    my $rs = $self->resultset();
    $rs->delete;
}
  • 解説
    • クラス名は$selfの末尾から取得
      • 完全にpixisのパクりである。でもこれは俺も思いついてたYO!
      • 毎回正規表現ってのがリソース気になるっちゃなる
    • スキーマはクラス変数に置いたので、そこから取得
    • newは使わないけど、無いとCatalyst::Model::MultiAdaptorが怒る
    • searchはアレイコンテキストで使うことがまずないので、search_rsでラップ
    • findも正直primary keyでしか検索しないので、そういうラップに。オレオレ仕様ですよ


これで全APIで、

$self->search({hoge => 'huga});

とかできる。
populateとかtxn_doも必要なら追加すればいい。

  • use String::CamelCase qw/decamelize/; は使ってないけど、何なの?
    • まぁ先を読んでください

コントローラで$c->model('API::User')じゃなくてUserで呼べるようにする

railsかぶれの小手先だけのせこい手法である。
オレオレActiveRecord以前のをちと修正しただけ。

package AutoTest::ActiveRecord;
use strict;
use warnings;

use DirHandle;
use base qw/Exporter/;

my @schemas;

use subs @schemas;
our @EXPORT = @schemas;

BEGIN {
    my $app_name = (split (/::/, __PACKAGE__))[0];
    my $path = $ENV{APP_HOME} || '';
    $path .= "./lib/$app_name/Schema";

    my $d = DirHandle->new($path);
    while (defined (my $f = $d->read)) {
        next if $f =~ /^\./;
        $f =~ s/\.pm//;
        push @schemas, $f;
    }
}

sub setup {
    my $c = shift;

    $c->NEXT::setup(@_);

    foreach my $F (@schemas) {
        *{$F} = sub { $c->model("API::$F") };
    }
}

1;


以前はここで find_by と find_all_by を名前空間を侵して実装してたけど、それはAPI::Base::DBICに移動。
no strict 'refs'; が消えた。
これで User と $c->model('API::User') が等価になったので、これをコントローラでuseすればいいと。
グローバル変数使ったり、色々酷いコードだ。

find_by(id => 3) とか find_all_by(name => 'foo') とかを実装

戻って AutoTest::API::Base::DBIC へ。上で書いたのは途中までだったのだ。

### original

sub find_by {
    my ($self, $column, $value) = @_;

    $self->find_all_by($column, $value)->next;
}

sub find_all_by {
    my ($self, $column, $value) = @_;

    my $class = decamelize $self->class();
    $self->search({"$class.$column" => $value});
};

1;


perlは => の左辺にはBarewordが使える。hoge(huga => 'foo') とすると、hogeの中ではkeyがhugaで、valueがfooのハッシュとして受け取れる。
だから

p 'find';
p User->find(4)->name;
p 'find_by';
p User->find_by(name => "ore")->name;
p 'find_all_by';
p User->find_all_by(age => 9)->count;

とできる。実行結果は

$VAR1 = 'find';
SELECT user.id, user.name, user.age, user.sex, user.created_at, user.updated_at FROM user user WHERE ( ( user.id = ? ) ): '4'
$VAR1 = 'user_ok';
$VAR1 = 'find_by';
SELECT user.id, user.name, user.age, user.sex, user.created_at, user.updated_at FROM user user WHERE ( user.name = ? ): 'ore'
$VAR1 = 'ore';
$VAR1 = 'find_all_by';
SELECT COUNT( * ) FROM user user WHERE ( user.age = ? ): '9'
$VAR1 = 8;

となる。