あるある ORM ドハマリ大辞典

tag perl sql orm

こちら Yappo の日でございますが、 Yappo の執筆ペースが芳しくないので、本日も社員のオオサワが代打で「DBI」や「ORM」について書かせて頂きたいと思います。

概要

同僚から良くネガティブだなで言われてる事で有名な私が、 ORM を使って複数人開発をする場合にハマるポイントをメモします。

trigger / hook point

insert, update, delete クエリの前後処理を拡張して、レコード作成時刻の設定や update 時刻の更新はたまたレコード削除時に削除テーブルへの自動コピー等を、一度ベースクラス上で定義しておけば新しく作るテーブルへも use parent する等して簡単に適用出来きるように便利になりますが、うっかりしてると後続の開発者がハマったり制約が出てきます。

DBMS の trigger

ORM の機能の trigger を多用していると、後続は DBMS 本体の trigger を使う事に躊躇します。使っちゃいけないというわけではないでしょうが、一つのクエリに対する副作用がある似たような機能を別レイヤーで使ってしまうと、トラブルが発生した時に ORM なのか DBMS に原因があるのかを切り分けるので混乱しそうです。

しまいにはお互いの副作用が混ざって酷いトラブルが起きてしまうかもしれないので「ああっ!ここで TRIGGER の定義できればラクなのに!!1」というシチュエーションでも泣く泣く諦めてしまいます。

Master Server に直接クエリを埋め込みにくい

ちょっとしたバッチ処理とかは shell script で書いてしまって、生 SQL を使ってデータを更新するとかやりたくなるケースは結構あると思います。

例えば ORM の insert trigger で「 url カラムに割り当てられた URL を HTTP GET して来てページタイトルを取得し title 絡むに埋め込む」的な定義がしてあるのを見落としてしまい、バッチ処理では url カラムにだけ値を入れていて後から「あれ?ページタイトルがない!」とか慌てるケースが出てくる可能性があります。この種のうっかりは注意してても発生してしまうので恐い所です。

道を外れると面倒

たとえば update 文の時は常に updated_at カラムを更新するという trigger が全テーブル共通処理として定義されていたとしましょう。まれに read_flg というカラムの更新した時だけは updated_at を更新したくないという要求が出てくる時があります。(まぁカラムを増やして対処するのが正しいのですが動いてるサイトにはそういう状況が出てくる物です。)
そういう時レアケースに対応しようとしだすと、別口でフラグを立てて bad code を書く事になったりとかアーキテクチャの変更を考えなければいけなくなったりとか大部面倒になります。

inflating

途中からプロジェクトに入ってて、あるカラムがオブジェクトに inflating されてたけど stringify っててマジきづかなかったわー!とかなってる人をよく見かけます。

transparent caching

そもそも透過キャッシュ自体があまり良くないものと言う認識が広まってますが、富豪的に作ってるターンの時は便利って言えば便利なんですよね。 Data::Model の当該機能は業務ではつかわんかったけど。

これだけ透過キャッシュ

もちろん、透過キャッシュすべきテーブルとしないテーブルとか別けて使う事になるんですけど、自分が作ったテーブルでもない限り意識的にならないとどのテーブルに透過キャッシュに入ってるか解らなくて、時たまデータが更新されてないと思ってビビる事がありますね。しかもエンジニアが絡まない view 側の変更をする人とかだともっと良くわからなくなるんじゃないかと思います。

too many memcach query

時たまキャッシュに入ってる事前提にして安心して

for my $id (@ids) {
    my $row = $orm->lookup($id);
}

とかしてて全部で2000クエリ位吐いてるページを見かけます。多分そのくらいのオーダーだと memcached でも問題でるんじゃねw
ORM でも油断しないように JOIN を使ってクエリ削減はしたいけど、透過キャッシュとかかましててそれが面倒い場合には最低でも select * from .. where ... で id list 引っ張って来た後に lookup_multi 的なくえりを叩いて DBMS memcached 双方に優しくなりたい物です。

has_a

うっかり使ってると view を書く人が糞程多くのクエリを投げてる事があります。使うなとは言いませんが、使われる view によっては lookup_multi 等で view に渡す前にデータを取ってこないとやばいですね。

れーすこんでぃしょんもんだい

一度 has_a したらキャッシュして二度同じクエリを投げないようにしてる時に、時たま

my $row1 = $orm->lookup(1);
my $row2 = $orm->lookup(1);
say $row2->has_a_table1->name;
$row2->has_a_table2->name('new name');
$row2->has_a_table2->update;
say $row2->has_a_table1->name; # あれれ new name になってないよ!

とかやっちゃう人が居ますが、その辺も注意がいりますね。

expanded row class

ベースとなる ORM の row class を更に便利にするべくプロジェクト独自拡張を入れまくると、新入りの学習コストが膨大に跳ね上がる傾向があります。
それを使っている側のコードを見るだけでは一瞬で解らなかったりとか、 ORM のドキュメント見ても書いてない機能で探したあげくにプロジェクトのリポジトリにコードがあったりとかっていう回り道をする時もあるのですね。

ORM は悪い子?

基本的には個人プロジェクトや少人数プロジェクト or 小規模負荷プロジェクトで使う分にはコスパが良いですが、規模が拡大するにつれ便利機能をうっかり使う事による過負荷、そもそもの教育コストが高くなる等の問題が出て来て業務向きではないと言われる事が多いですが、プロジェクトに関わるエンジニアの技術水準を上げておけば大概の問題は出にくいので気にしなくても医院じゃないでしょうか。

じゃぁやっぱり生が一番いいってことじゃないか!??って事を言う人も出るでしょうが、結局生でやってても ORM が持つ要素の抽象化が欲しくなって来て俺俺 ORM 作りしだす事もあるんで、この銀の弾丸でも何でも無い劇毒さんの良い所と悪い所をちゃんと頭に叩き込んだ上で付き合うのが今を生きる正しい Web エンジニアの姿だと思うのですよ。はい。

まとめ

転職してからハマったことや、自分で作っといて忘れてポカした所のメモ帳にだいぶ脚色をした上で後悔しました。