[Perl] DBIx::Class(DBIC)でtimestamp的なカラムの自動更新

[Perl] DBIx::Class(DBIC)でtimestamp的なカラムの自動更新

今更ながらDBIx::Class(DBIC)に入門。使い始めてDBICの作法に翻弄されてる。

timestamp的なカラム、例えばupdated_atカラムを自動更新させるのにハマったのでメモ。

package Jobeet::Schema::ResultBase;
use strict;
use warnings;
use parent 'DBIx::Class::Core';

__PACKAGE__->load_components(qw/InflateColumn::DateTime/);

sub insert {
    my $self = shift;

    my $now = Jobeet::Schema->now;
    $self->created_at( $now ) if $self->can('created_at');
    $self->updated_at( $now ) if $self->can('updated_at');

    $self->next::method(@_);
}

sub update {
    my $self = shift;

    if ($self->can('updated_at')) {
        $self->updated_at( Jobeet::Schema->now );
    }

    $self->next::method(@_);
}

1;

上記のようなResultクラスのベースを継承してResultクラスを作っていくのだけど、ちょっとした問題に当たる。

先程のエントリーの例で Jobeet::Schema::Result::Job のデータを更新するには下記のようになる。

# その1
$schema->resultset('Job')->find($id)->update({ is_public => 1 });

# その2
$schema->resultset('Job')->search({ id => $id })->update({ is_public => 1 });

いずれのコードもis_publicカラムを1に更新する。そして、updated_atカラムが現在の時刻に自動更新される...と思うのだけどそうならない場合がある。

更新されるのはfind()を使っているその1の場合だけ。その2の場合は更新されない。

この理由と対処方法は下記エントリーに書かれている。

find()search()は帰ってくるオブジェクトが違う = 使われるupdateメソッドは別のものって事。

DBICなら用意されていそうだと思って探したら DBIx::Class::TimeStamp っていうのを見つけたけど、こちらもfind()のようにrowオブジェクト取得してからupdateしないと効かないようだ...。

search()->update()を使う理由

単純に発行されるSQLが少ないから。

今回やりたいことは更新対象の行のprimary keyが分かってて、かつ、その行のデータの取得は必要がない場面を想定している。

発行されるSQLを見ながらいろいろ試してみて、$schema->search({...})->update({...}) の場合はSELECT文が発行されず、UPDATE文のみが発行されるようだったので。

通常はいきなりUPDATEを行うことは少ないかもしれない。

解決策

解決策としてはfind()で更新するって方法とResultクラスではなくResultsetクラスのベースになるものにしてあげるって方法があると思う。

find()で解決した場合の問題点

find()を使う場合は必ずSELECTが発行される。先程の例で言うと発行されるSQLのイメージは下記のような感じ。

SELECT * FROM job WHERE id = xxx;
UPDATE job SET is_public = 1, updated_at = xxx WHERE id = xxx;

必要がないSELECT文が実行されるのは避けたい。

ResultSetクラスでの解決

Resultクラスの共有部分を作るように、ResultSetクラスの共有で解決できそうだと思って試したら出来た。

03日目: データモデル の例でのサンプルコード

schemaクラス

package Jobeet::Schema;
use strict;
use warnings;
use parent 'DBIx::Class::Schema';
use DateTime;

__PACKAGE__->load_namespaces(
    default_resultset_class => '+Jobeet::Schema::ResultSetBase'
    # or
    # default_resultset_class => 'ResultSetBase'
);

my $TZ = DateTime::TimeZone->new(name => 'Asia/Tokyo');
sub TZ    {$TZ}
sub now   {DateTime->now(time_zone => shift->TZ)}
sub today {shift->now->truncate(to => 'day')}

1;

load_namespaces の引数 default_resultset_class でデフォルトの ResulutSet クラスを指定。

ResultSetクラス

package Jobeet::Schema::ResultSetBase;
use strict;
use warnings;
use parent 'DBIx::Class::ResultSet';

# ResultBaseクラスのinsertで対応可能なので必要ない
# sub create {
#     my $self = shift;
# 
#     my $table = $self->result_source;
#     my $now   = Jobeet::Schema->now;
#     $_[0]->{created_at} = $now if $table->has_column('created_at');
#     $_[0]->{updated_at} = $now if $table->has_column('updated_at');
# 
#     $self->next::method(@_);
# }

sub update {
    my $self = shift;

    if ( $self->result_source->has_column('updated_at') ) {
        $_[0]->{updated_at} = Jobeet::Schema->now;
    }

    $self->next::method(@_);
}

1;

Resultクラス

package Jobeet::Schema::ResultBase;
use strict;
use warnings;
use parent 'DBIx::Class::Core';

__PACKAGE__->load_components(qw/InflateColumn::DateTime/);

sub insert {
    my $self = shift;

    my $now = Jobeet::Schema->now;
    $self->created_at( $now ) if $self->can('created_at');
    $self->updated_at( $now ) if $self->can('updated_at');

    $self->next::method(@_);
}

sub update {
    my $self = shift;

    if ($self->can('updated_at')) {
        $self->updated_at( Jobeet::Schema->now );
    }

    $self->next::method(@_);
}

1;

このResultクラスを継承して各テーブルのResultクラスを作る。エントリーの例で言うと下記コードのような感じ。

package Jobeet::Schema::Result::Job;
use strict;
use warnings;
use parent 'Jobeet::Schema::ResultBase';

# ここにテーブル定義

1;

もうちょっとスマートな方法があったら後で加筆する。