DBIx::Classで論理削除
DBICで論理削除をしたくなったので調べていたのだが、うまく書く方法がイマイチなかった。
まず、削除フラグを常にチェックするようにするのは簡単で、テーブルクラスに
__PACKAGE__->resultset_attributes({ where => { deleted => undef }});
とか書いてくだけでつねにWHERE句に deleted IS NOT NULL
が入るようになる。これはマニュアルに書いてある通り。
問題は削除するときで、テーブルクラスで delete
定義してそこで update({ deleted => 1 })
とかやればいいかなと思いきや、そうすると cascade delete 効かなくなってしまっていやだ。
DBICのrowに対するdeleteチェーンは大まかに
- ユーザー定義テーブルクラスでのdelete (定義されてる場合)
- DBIx::Class::Relationship::CascadeActions
- DBIx::Class::Row
となっていて、2 で cascade delete の処理が入り。3で実際に row が削除される。
(正確には2ではnextよんでからcascade deleteしてるため 1 -> 2 -> 3 -> 2 という感じである)
それでこの場合は実際に削除するのを update({ deleted => 1 })
に置き換えたいので 3 の直前に自分のメソッドを差し込んでそこでチェーンをとめるという実装がしたいと思った。
のだけどしばらく考えたけどいいやり方が見つからなかったので結局 1 のユーザー定義クラスで
sub delete {
my $self = shift;
$self->update({ deleted => 1 });
my $source = $self->result_source;
$self->search_related($_)->delete_all
for grep { $source->relationship_info($_)->{attrs}{cascade_delete} } $source->relationships;
$self;
}
などと全部詰め込む感じでお茶を濁した。
論理削除を今まで使ってなかったのでいまさら感がありますが、DBICで論理削除ってるひとでいいやり方知ってる人いたら教えてください!
DBIx::Class::AsArrayHash - Hatena::Diary::Neko::kak 500 Internal Server Error
うんうん、DBICつかってるとmapまくりよね。僕ならこう書いてるな。
my @rets = map { $_->get_columns }, $rs->all;
DBIC::AsArrayHashいらないってのは同意。
retrieveとかCDBI的なのもあれ。
舌足らずすぎた。
Model::DBIC:
connect_info:
- dbi:mysql:table
- root
- on_connect_do:
- SET NAMES utf8
cursor_class: DBIx::Class::Cursor::Cached
cache_file: __path_to(tmp/query_cache)__
さっきはこんなconfigで使った場合のコードです。
DBIx::Class::Cursor::Cachedつかってみた
すばらしすぎる。もっと早く使えばよかったとおもった。
Catalyst::Model::DBIC::Schema
で使う場合はこんな感じでOK。
sub new {
my $self = shift->NEXT::new(@_);
my $cache = Cache::FastMmap->new( share_file => $self->{cache_file} );
$self->schema->default_resultset_attributes({
cache_object => $cache,
});
$self;
}
そんで、あとはsearchのattrとして { cache_for => 300 } とかかいてやればそのクエリは300秒キャッシュされる。
実際にはsearch時ではなくてallとかnextのときにそのクエリをキャッシュしてる。
ちなみにdefault_resultset_attributes
にcache_for
を含めるとすべてのクエリがキャッシュされる。
書き忘れたけどさっきの update_schema.pl
は
./script/myapp_update_schema.pl dbi:mysql:tablename username password
見たいな感じで DSN を渡す必要がある。めんどう。
ここら辺があれでまだヘルパーにはなってない。
Schema::Loader 使い方
僕の中で流行ってる使い方があるのでかぶせて書いておいてみる。mizzy さんの二個目の例を自分ルール化させた感じ?
作業は一般的なCatalystアプリのディレクトリ構造上であるとして、そこに新しく schema というディレクトリを作成。
そんで、
- schema/lib/Schema/{TableName}.pm
に各テーブルのリレーション定義とかメソッドとかを自分で書く。実際にこのライブラリは Catalyst にロードされない。
上記ファイルを元に Schema::Loader
の make_schema_at
を使い
- lib/MyApp/Schema.pm
- lib/MyApp/Schema/*.pm
にコードを自動生成して、そちらをロードするという感じ。こちらのコードは自分では書き換えない。
コード自動生成は script/myapp_update_schema.pl
にこんなのを書いてそれを実行している。
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin;
use File::Spec;
use lib File::Spec->catfile( $FindBin::Bin, qw/.. schema lib/ );
use DBIx::Class::Schema::Loader qw/make_schema_at/;
die unless @ARGV;
make_schema_at(
'MyApp::Schema',
{ components => ['ResultSetManager', 'UTF8Columns'],
dump_directory => File::Spec->catfile( $FindBin::Bin, '..', 'lib' ),
dump_overwrite => 1,
debug => 1,
},
\@ARGV,
);
@INC
に schema/lib
をくっつけてから make_schema_at
してるだけ。ワンライナーでもできるけど見通しが悪いので。
これで普通の make_schema_at
でつくられる Schema ファイルに自分で schema/lib/Schema/ 以下に書いた定義がくっついて出力されるという寸法。
Schema::Loader
でダイナミックロードを使っていると各テーブルクラスにはリレーション定義やメソッド拡張のコードだけをかけばいいのでシンプルになって好きなんだけど、そうすると起動時のオーバーヘッドがあるし、ResultSetManagerがつかえなくていやんというときに、これだとまぁイイとこどりのような感じにできる。
実際にいじるファイルは schema/lib 以下のファイルで、それらのファイルを更新するたびに update.pl を実行するというのがあれだけどまぁ自動化できる。してないけど。
難点は、schema/lib 以下のファイルでsyntax errorがあっても何も言われないこと。
何も言われないというか、syntax error があるファイルは
# Loaded external class definition for 'MyApp::Schema::TableName'
ってのが出ないだけという。どこがエラってるのかとかがわからないので、がーーって書いてどこか typo してたりするとはまるかも。
inflate_column + time_zone
んー
for my $date_column (qw/created_date modified_date/) {
__PACKAGE__->inflate_column(
$date_column => {
inflate => sub { DateTime::Format::MySQL->parse_datetime(shift)->set_time_zone('UTC') },
deflate => sub { DateTime::Format::MySQL->format_datetime(shift->set_time_zone('UTC')) },
}
);
{
no strict 'refs';
*{"$date_column\_for"} = sub {
my ($self, $user) = @_;
($user && $user->info && $user->info->time_zone)
? $self->$date_column->set_time_zone( $user->info->time_zone )
: $self->$date_column;
}
}
}
あけましておめでとうございます!
今年初良いことは、DBICのinflate_result
上書きによるサブクラス化を知ったことです!
うおおやっべぇ便利すぎる。
詳しくは
DBIx::Class::Manual::Cookbook - Miscellaneous recipes - search.cpan.org
参照のこと。
知らなかったなぁ。いつから使えたのか。
make_schema_at が external クラスも読むようになっている
既存のDBに接続してテーブル情報などを取得して、DBIx::Class::Schema のテーブルクラスを作成してくれる Schema::Loader の make_schema_at
が自分で定義した外部クラスファイルも見てくれるようになっていた。
perl -Ilib -MDBIx::Class::Schema::Loader=make_schema_at,dump_to_dir:tmp -e 'make_schema_at("MyApp::Schema", { components => [qw/ResultSetManager UTF8Columns/]}, ["dbi:mysql:myapp","root"])'
などとすると、./tmp にスキーマクラスをダンプしてくれるのだけど、-Ilib している中にすでに MyApp::Schema::User とかがかいてあると、それも作成するクラスにくっつけてくれる。
便利だ。