カテゴリー
technology

MT::Blog::clone()の罠

仕事でMTをごにょごにょいじってる。バッチ処理中にMT::Blogのclone()を使ってガンガン作成しようとするとエラーが出た。

DBD::mysql::st execute failed: Column 'templatemap_template_id' cannot be null at /path/to/MT/extlib/Data/ObjectDriver/Driver/DBI.pm line 348, <> line 4.

管理画面のブログ一覧から「ブログの複製」を実行すると特に警告もなく終了する。もちろんログに何も出ていない。
いろいろ調査していくとMT::Blog::clone()で、え?と思う実装が。(以下ソースは 4.23 のもの)

require MT::Template;
$iter = MT::Template->load_iter(
{ blog_id => $old_blog_id, type => { not => 'widgetset' } }
);
my $tmpl_processor = sub {
my ( $new_blog_id, $counter, $tmpl, $tmpl_map ) = @_;
$callback->($state . " " . MT->translate("[_1] records processed...", $$counter), 'tmpls')
if $counter && ($$counter % 100 == 0);
my $tmpl_id = $tmpl->id;
$$counter++;
delete $tmpl->{column_values}->{id};
delete $tmpl->{changed_cols}->{id};
# linked_file won't be cloned for now because
# new blog does not have site_path - breaks relative path
delete $tmpl->{column_values}->{linked_file};
delete $tmpl->{column_values}->{linked_file_mtime};
delete $tmpl->{column_values}->{linked_file_size};
$tmpl->blog_id($new_blog_id);
$tmpl->save or die $tmpl->errstr;
$tmpl_map->{$tmpl_id} = $tmpl->id;
};
$counter = 0;
while (my $tmpl = $iter->()) {
$tmpl_processor->($new_blog_id, \$counter, $tmpl, \%tmpl_map);
}

パッチでも送りつけようかと思ったけど、とりあえず普通にCGI経由で使っていれば問題がないので(顕在化しない)ブログに公開する程度にする。


さて他のオブジェクトもこのような実装がされているんだが、簡単にまとめると複製元となるブログからオブジェクトを読み出して
・プライマリーキー削除
・プライマリーキー変更記録削除
・新しいブログIDをセット
・保存(新規プライマリー値設定)
たぶん Data::ObjectDriver あたりがキー値がないものは新規扱いにして、INSERT 文発行してくれているのでしょう。コレ単体ではいい感じの動きかもしれないけど、その後もう一度同じ条件で load_iter() を実行すると予想に反した結果が返る。
もう一つ Data::ObjectDriver のいい感じの機能としてキャッシュ機構がある。先の複製方式だと同一検索条件向けのキャッシュのデータをいじっていることになるので、同じ検索条件でついさっき複製・更新して保存した別のブログのためのデータを拾う結果となる。
テンプレート以外にもエントリーやカテゴリーなどの複製もあるので同様に問題が発生するかと思ったら、値にだけに着目すれば複製結果を複製しても実害がない。
ところがテンプレートマップ(どのテンプレートがどのブログ)を更新するときに問題が発生する。テンプレートを複製するときに複製元からコピーして新しいブログ用のテンプレートを保存する際、あとでブログとテンプレートIDのマッピングを保存するためにオリジナルのIDと複製先のIDの対応をハッシュ %tmpl_map へ記録してる。
ブログ複製の二回目以降、本来はオリジナルのブログのテンプレートIDが複製元にならなくちゃいけないのに、キャッシュから取り出されたデータは直前に更新され保存されたデータを参照してしまう。つまり直前に処理したブログのテンプレートIDがなぜかオリジナルのものとして利用され、その値をキーとして複製後のIDが保存される。
その後テンプレートマップを保存する際に問題が発生する。

require MT::TemplateMap;
$iter = MT::TemplateMap->load_iter({ blog_id => $old_blog_id });
$counter = 0;
while (my $map = $iter->()) {
$callback->($state . " " . MT->translate("[_1] records processed...", $counter), 'tmplmaps')
if $counter && ($counter % 100 == 0);
$counter++;
delete $map->{column_values}->{id};
delete $map->{changed_cols}->{id};
$map->template_id($tmpl_map{$map->template_id});
$map->blog_id($new_blog_id);
$map->save or die $map->errstr;
}

そもそも %tmpl_map は本来の意図した状態とは違うことになっていて、その壊れたハッシュにオリジナルのテンプレートIDをキーにして値を参照してる($tmpl_map{$map->template_id}) ため存在しないキー値でのアクセスとなり、値は無しとなる。当然 template_id は NOT NULL なので $map->save 時にDBDでエラー発生。その結果先の冒頭のエラーメッセージが発生する。
個別のオブジェクトにも clone() があるんだから、一旦複製してから保存すればいい。ということで、こんな感じでOK。

my $new_tmpl = $tmpl->clone;
delete $new_tmpl->{column_values}->{id};
delete $new_tmpl->{changed_cols}->{id};
# linked_file won't be cloned for now because
# new blog does not have site_path - breaks relative path
delete $new_tmpl->{column_values}->{linked_file};
delete $new_tmpl->{column_values}->{linked_file_mtime};
delete $new_tmpl->{column_values}->{linked_file_size};
$new_tmpl->blog_id($new_blog_id);
$new_tmpl->save or die $new_tmpl->errstr;
$tmpl_map->{$tmpl_id} = $new_tmpl->id;

なんか言葉で説明難しいけど、書くだけ書いた。疲れた。