正しく保存するためのモデリング
「データ整合性を保つ非同期処理アーキテクチャパターン」というタイトルでセッションを始めたのは、スマートバンクのエンジニア・木田悠一郎氏。担当するのは、カード決済や後払いといった金融領域の開発・運用だ。発表では同社のカード発行処理を例に、非同期処理における設計課題とその解決パターンが紹介された。
スマートフォンアプリからカード発行を申請し、物理カードが発行・発送され、手元に届いたのちにアプリ上で利用開始する。こうした一連の業務プロセスは、複数の「イベント」で構成される。そして、これらのイベントは非同期処理によってつながっているため、「処理の流れが把握しづらい」「変更が難しい」「データ整合性の担保が困難」といった課題が起きがちだと木田氏は説明する。
こうした課題を乗り越えるための設計方針として、木田氏は次の4つのポイントを提示する。
- イベントを正しく保存する
- イベント間の整合性を保つ
- データベースとメッセージキューの整合性を保つ
- メッセージのスキーマを定義する
まず解説されたのは、「イベントを正しく保存する」ためのモデリング手法だ。
「非同期処理の前後には、必ずといっていいほど何らかのデータ保存が発生する」と木田氏。だからこそ、まずは構造をしっかり設計することが大切になるという。その際に重要になるのが、「イベント」と「リソース」を分けて捉えるという考え方だ。
たとえば、「受注日」「発送日」「入金日」など、時間とともに一度だけ発生する事象は「イベント」に分類される。一方で、従業員や商品など、持続的に存在する情報は「リソース」として区別されるべきだ。「イベントとリソースをごちゃ混ぜにすると、更新処理が煩雑になり、テーブル構造も不明瞭になります」と木田氏は語る。
スマートバンクにおけるカード発行処理を例に、木田氏は4つのモデリングパターンを紹介する。
【前提:カード発行に関わる一連の処理】
- カード発行申請:ユーザーID、カード名義、申請日時など
- カード発行:カードIDなど
- カード発送:配送先住所など
- カード利用開始:利用開始日時など
【モデリングパターンとその特徴】
パターン1:直前のイベントIDを持つ
各イベントに、直前のイベントIDを持たせて順序を明示する方法。外部キー制約により厳格な順序管理が可能になる。一方で、全体の流れをたどるにはイベントを一件ずつ追う必要がある。
パターン2:最初のイベントIDを持つ
すべての後続イベントに「カード発行申請」など最初のイベントIDを持たせる方式。イベント分岐にも対応しやすく、関連データを一括取得しやすい。ただし、順序の保証はアプリケーション側に委ねられる。
パターン3:ロングタームイベントを親に持つ
すべてのイベントの親となる「カード注文」などのテーブルを用意し、現在のステータスをその中に保持する設計。業務の進捗をひと目で把握でき、複雑な業務フローにも対応しやすい。
パターン4:ツリー構造(経路列挙モデル)
イベントのIDをスラッシュで連結し、履歴を1カラムに格納する方法。たとえば「申請ID/入金待ちID/カード発行ID」のように、階層構造を1行で持つことができる。RubyではancestryというGemを使えば簡易に実装可能。
いずれのパターンにおいても、イベント同士の関連づけが鍵になる。「フローに分岐がある場合や、処理の進行に応じて複雑化していく場合には、どの方式を選ぶかがシステムの柔軟性を左右する」と木田氏は話す。
一方で、現場でしばしば見かけるNGパターンとして「全部盛りモデル」が挙げられた。これは、複数の概念や責務を1つのテーブルに詰め込んでしまう設計であり、「一見楽に見えても、更新処理が煩雑になり、変更や拡張に弱くなる」と警鐘を鳴らす。
「テーブルを4つに分けると逆に複雑になるのでは?」という懸念に対しても、木田氏は「分けることで仕様変更に対応しやすくなる」と説明。たとえば、「複数のカード発行申請に1つの発行を紐づけたい」といった要件変更にも柔軟に対応できるようになるという。
パフォーマンスやクエリの複雑さについても、「高いパフォーマンスが求められるケースは実はそこまで多くない」と補足。むしろ、更新対象テーブルを分けることでロック競合を防ぎ、保守性が高まる場面の方が多いと述べた。
