Active Record と mini_sql の混合による予期せぬ動作

SQL ログを確認している際、Active Record と mini_sql の使い方が混在しているため、一部のクエリがコードの意図と整合していないことに気づきました。

レビュー可能なオブジェクト(reviewables)を更新しようとする際、競合状態の可能性を防ぐためにトランザクション内でバージョンをインクリメントしています(以下のコードスニペット参照)。インクリメントがトランザクションの一部のように見えますが、実際にはそうではありません。

ここで使われている Reviewable.transaction は Active Record の機能ですが、increment_version! は mini_sql を使用しています:

Active Record は mini_sql が何を行っているかを認識していません。Active Record は遅延評価のような機能を使用しており、トランザクション内の最初のクエリが発行される直前に BEGIN が発行されるようです。その結果、バージョンのインクリメントが先に行われ、その後に Active Record からの最初のクエリが発行される直前にトランザクションが開始されます。処理は以下のようになります:

UPDATE reviewables SET version = version + 1 WHERE version=version AND id = reviewable.id RETURNING version;
BEGIN;
...

トランザクション外でバージョンがインクリメントされるため、楽観的ロックによって提供される保証が得られません。インクリメントがトランザクションと原子性を共有していないことが原因です。

「いいね!」 4

興味深いですね。AR に通知するためにミニ SQL を教えるべきです。確かに修正の価値があります。

「いいね!」 3