Vegas Engineering by FORBIS 記事一覧
← 記事一覧へ戻る
Data ・ 第20回

移行データの落とし穴 — "起点"のつもりが"最新"に化ける参照ミスと、循環参照

一括生成した親子リンクの取り違えは、後段の連鎖処理を暴発させる。実際に「注文が突然消える」まで起きうる。防ぎ方を整理する。

Vegas 開発チーム ・ 2026-07-03 ・ 読了 7分

CONTEXT参照リンクは業務の背骨である

通販基幹のデータには、エンティティ間に親子や関連の参照リンクが張られている。定期が受注を生み、受注が出荷を生む——といった具合に、あるレコードが「どこから生まれたか」「何に紐づくか」を指すフィールドが随所にある。これらは単なるメタ情報ではなく、状態変更やキャンセルの伝播経路そのものだ。だからこそ、リンクの向きと起点を正しく保つことが、業務ロジックの正しさに直結する。

PITFALL"起点"のつもりが"最新"に化ける

典型的な事故は、移行や一括生成の場面で起きる。本来「起点(最初に生成した元)」を指すべき参照フィールドに、集約処理の都合で「最新値」が入ってしまうのだ。生成SQLで対象を束ねる際、うっかり max() で最も新しいレコードを拾ってしまうと、親が「自分が後から生成した子」を指すことになる。こうして A→B(親→子)と B→A(子→親)が同時に成立する、双方向すなわち循環リンクが混入する。

一件ずつ見れば正しく紐づいて「見える」ため、移行直後のスポットチェックはすり抜けてしまう。矛盾は個々のリンクではなく、リンク同士の関係——閉路——に潜んでいる。

正常:一方向 親(起点) 生成元 状態変更は下流へ一度だけ流れる 異常:循環 親(起点) 状態変更が循環で戻り、伝播が止まらない
図:一方向なら伝播は一度きり。逆向きの誤エッジが混じると閉路ができ、変更が戻り続ける。

IMPACT連鎖が暴発し、レコードが「消える」

循環リンクの怖さは、静的な矛盾では終わらない点にある。キャンセルや削除といった状態変更は、リンクを辿って関連レコードへ伝播する設計であることが多い。ところが閉路があると、伝播は親から子へ、子から親へと戻り、ループに陥る。

データそのものは移行時に「作れて」いた。だが参照整合、とりわけ循環の検証を省いたために、運用開始後の連鎖処理が引き金を引いたのである。

起点の取り違え:最古を採るべきか、最新を採ってしまうか レコード(最古) = 本来の起点 中間 レコード(最新) max()が拾う値 正:min()で最古を起点に 誤:max()で最新を起点に
図:集約で「最新」を拾うと起点が反転する。起点は最古(最小)か明示フラグで判定する。

DETECT双方向リンクを名指しで洗い出す

検出の要点は「A→B かつ B→A」の対を見つけることだ。参照テーブルを自己結合し、向きが逆の同じペアが両方存在する行を抽出すれば、循環は機械的に列挙できる。まず全数を数え、影響範囲はサンプリングで見積もる。

detect_cycle.sqlSQL
SELECT a.parent_id, a.child_id
FROM   ref_link a
JOIN   ref_link b
  ON   b.parent_id = a.child_id   -- 逆向きの相手を突き合わせる
 AND   b.child_id  = a.parent_id
WHERE  a.parent_id < a.child_id;  -- 同一ペアの重複出力を防ぐ(& 自己参照も除外)

FIX起点を厳密化し、閉路を切る

そして最大の保険は、全操作の履歴を残しておくことだ。異常が起きても、どの操作がどのレコードをどう変えたかを操作単位で再構成でき、原因の切り分けが一気に楽になる。

TAKEAWAY

移行は「作れた」で終わらせない。参照の"起点"は最古か明示フラグで定義し、集約で最新を拾わせない。双方向リンクの検出クエリを完了条件に組み込み、循環ゼロを確認してからリリースする。履歴があれば、事故は必ず追える。