[Perl] DBIx::Class(DBIC)でtimestamp的なカラムの自動更新
- 2017.03.06
- perl
- DBIC, DBIx::Class, ORM, perl
今更ながらDBIx::Class(DBIC)に入門。使い始めてDBICの作法に翻弄されてる。
timestamp的なカラム、例えばupdated_at
カラムを自動更新させるのにハマったのでメモ。
- 03日目: データモデル から抜粋
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;
もうちょっとスマートな方法があったら後で加筆する。
-
前の記事
Mojoliciousでactionの前後に実行される独自のHookを作る。before_action、after_action編 2016.12.07
-
次の記事
Gunma.web#27は4月29日だって! 2017.03.30