移行データの落とし穴 — "起点"のつもりが"最新"に化ける参照ミスと、循環参照
一括生成した親子リンクの取り違えは、後段の連鎖処理を暴発させる。実際に「注文が突然消える」まで起きうる。防ぎ方を整理する。
CONTEXT参照リンクは業務の背骨である
通販基幹のデータには、エンティティ間に親子や関連の参照リンクが張られている。定期が受注を生み、受注が出荷を生む——といった具合に、あるレコードが「どこから生まれたか」「何に紐づくか」を指すフィールドが随所にある。これらは単なるメタ情報ではなく、状態変更やキャンセルの伝播経路そのものだ。だからこそ、リンクの向きと起点を正しく保つことが、業務ロジックの正しさに直結する。
- 親子リンク:生成元(起点)を指す。「この受注は、どの定期から作られたか」。
- 関連リンク:業務上の紐づけを指す。「この出荷は、どの受注に対するものか」。
PITFALL"起点"のつもりが"最新"に化ける
典型的な事故は、移行や一括生成の場面で起きる。本来「起点(最初に生成した元)」を指すべき参照フィールドに、集約処理の都合で「最新値」が入ってしまうのだ。生成SQLで対象を束ねる際、うっかり max() で最も新しいレコードを拾ってしまうと、親が「自分が後から生成した子」を指すことになる。こうして A→B(親→子)と B→A(子→親)が同時に成立する、双方向すなわち循環リンクが混入する。
一件ずつ見れば正しく紐づいて「見える」ため、移行直後のスポットチェックはすり抜けてしまう。矛盾は個々のリンクではなく、リンク同士の関係——閉路——に潜んでいる。
IMPACT連鎖が暴発し、レコードが「消える」
循環リンクの怖さは、静的な矛盾では終わらない点にある。キャンセルや削除といった状態変更は、リンクを辿って関連レコードへ伝播する設計であることが多い。ところが閉路があると、伝播は親から子へ、子から親へと戻り、ループに陥る。
- ある操作が起点のレコードを取り消す。
- 取消が誤った逆向きエッジを辿り、本来無関係なレコードへ波及する。
- 結果、あるレコードが突然「削除」「取消」状態に落ち、業務側からは「注文が消えた」ように見える。
データそのものは移行時に「作れて」いた。だが参照整合、とりわけ循環の検証を省いたために、運用開始後の連鎖処理が引き金を引いたのである。
DETECT双方向リンクを名指しで洗い出す
検出の要点は「A→B かつ B→A」の対を見つけることだ。参照テーブルを自己結合し、向きが逆の同じペアが両方存在する行を抽出すれば、循環は機械的に列挙できる。まず全数を数え、影響範囲はサンプリングで見積もる。
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起点を厳密化し、閉路を切る
- 生成ロジックの厳密化:起点は「最古(最小ID/最小日時)」または明示的な種別フラグで判定する。集約時に
max()で最新を拾わない。 - クリーニング:検出した対のうち、逆向きの誤エッジ(子→親)だけを削除して閉路を切る。正しい親→子は残す。
- 影響見積もり:切る前にサンプルで下流の連鎖範囲を確認し、想定外の巻き添えがないか点検する。
- 再発防止:移行の完了条件に「循環ゼロ」を含め、リリース前チェックとして自動化する。
そして最大の保険は、全操作の履歴を残しておくことだ。異常が起きても、どの操作がどのレコードをどう変えたかを操作単位で再構成でき、原因の切り分けが一気に楽になる。
移行は「作れた」で終わらせない。参照の"起点"は最古か明示フラグで定義し、集約で最新を拾わせない。双方向リンクの検出クエリを完了条件に組み込み、循環ゼロを確認してからリリースする。履歴があれば、事故は必ず追える。