« 企業システムの基本構造を理解しよう | トップページ | DBMSの方言を開発基盤で吸収する »

2012.01.12

バリデーションの双方向性問題を解く

 何回かにわたって「動的参照関係」における整合性に関する問題をとりあげた。とくに、「参照先」が更新された場合に起こり得る不整合について説明した。ただしそれらは「静的参照関係(通常の外部キー制約)」を含め、あくまでも「参照レコードが存在するかどうか」の問題である。

 しかし、「参照レコードが存在するかどうか」は多彩な制約の一部でしかない。それ以外にもさまざまな制約があって、それらすべてについて「参照先レコードが更新された場合の不整合問題」がつきまとう。「参照元」におけるバリデーション(制約にもとづく整合性の検証)はこれまでまともに対処されてきたが、何度か書いたように、「参照先」をバリデーションの起点とする不整合問題は見過ごされがちだった。これを、参照元と参照先の双方で対処すべき問題という意味で「バリデーションの双方向性問題」と呼ぶことにしよう。

 具体例を見よう。たとえば次のモデルのテーブルCにおいて「追加・更新時には、C.d <= A.b であること」という制約が存在するとしよう(C.dとは、テーブルC上のフィールドdの意味)。Cのレコードr2において、dが3500より大きい値で更新されようになったら、システムはそれをエラーとしなければいけない。ありきたりな制約だし、どんなやり方であってもバリデーションの実装も難しくはない。

 [A] {a}, b
 + a1 1500
 | a2 3500 ... (r1)
 |
 └―…[C] {c}, a, d
        c1 a2 3200 ... (r2)

 この制約関係を、参照先である[A]側から眺めてみよう。Aのレコードr1のbが、3500から3000に変更されそうになったらどうか。システムはそれをエラーとして更新を禁止しなければいけない。なぜなら「C.d > A.b」であるようなCレコードr2が存在することになるからだ。この更新を許せばデータの不整合が生じてしまう。

 レコードr1を更新するだけなのであれば、これに関連しているレコードr2については無視しておけばいいと思われた方がいるかもしれないが、そうはいかない。この制約が「追加時には、C.d <= A.b であること」ではなく、「追加・更新時には、C.d <= A.b であること」である点に注意してほしい。前者であれば無視していいが、後者であれば有効な参照元レコードが存在する限り、それとの整合性を維持する必要がある。

 昔、この話題を何人かの技術者に振ったことがある。そのときは「それはAの"バージョン"が変わったということにすれば無視していい」とか「そういうことにならないように、Aには有効期間を設ける必要がある」などとコメントされてがっかりしたものだ。それらが何の解決にもならないことがおわかりだろうか。バージョン管理をしようが、有効期間を設けようが、現時点で有効であるはずのr2が「古いr1」を参照したままでいいということにはならない。問題を曖昧にするだけの話だ。

 くどいようだが、「追加時のみに適用されるべきバリデーション」というものは確かに存在する。そのいっぽうで、「常に(つまり当該レコードが有効である限り)適用されるべきバリデーション」も存在する。そして、後者のタイプが前者に比べて少数であるとか例外的であるような印象は私にはない。上記の制約もごくふつうの例なのである。

 では、Aのレコードを更新する際に適用されるべきバリデーションをどのように実装すればよいのだろう。Aの保守プログラムに「C.d > A.bであるようなCレコードが存在すればエラー」とするようなコードを組み込めばよい。――これまではそのように対処されていたが(そもそも対処されないケースのほうが多かったような気がするが)、それはもうやめよう、というのが私の提案だ。似たようなロジックがあちこちに置かれてしまうし、コーディング量がちっとも減らないからだ。

 どうすべきか。まず、保守プログラム毎にバリデーション・コードを用意するやり方をきっぱりとやめてしまう。そのうえで、個々のテーブルにまつわるすべてのバリデーションをテーブルの拡張定義として一元的に登録できるようにする。たとえばテーブルCに対しては「追加・更新時には、C.d <= A.b であること」を組み込めるようにする。

 そのうえで、テーブルの保守プログラムを実行したときに、当該テーブルの拡張定義からバリデーションの内容が動的に読み込まれて実行されるようにする。Cの保守プログラムであれば、Aを適宜読み出しながらC.dの新しい値を評価することになる。ここまでは、外見上はこれまでと変わらない。このあとの動きが重要だ。

 すなわち、当該テーブルだけでなく「当該テーブルを参照先とするテーブル」からもバリデーション定義が読み込まれるようにする。Aの保守プログラムであれば、参照元であるCに対するバリデーション定義にもとづいてCを適宜読み出しながらA.bの新しい値を評価することになる。かくして双方向のバリデーションが、単一のバリデーション定義にもとづいて実行される。双方向を別々にコーディングする必要がない。

 ただし、パフォーマンスについては配慮が要りそうだ。じっさい私は自作の開発基盤上で双方向バリデーションの厳密化を進めているのだが、多くのテーブルによって参照されているマスター系テーブルの場合、バリデーションクラスの動的生成に非常に時間がかかることがわかった。たとえば「商品マスターの保守」のようなプログラムを起動しようとすると、商品マスターが24個ものテーブルによって参照されているために、バリデーションクラスの生成だけで10秒以上かかってしまう。

 更新ボタンを押してからバリデーションに多少時間がかかるのならまだガマンできるが、保守プログラムの最初のパネルが表示されるまでにいちいち10秒以上待たされるのはたまらない。そこで次のリリースでは、バリデーションクラスの生成を別スレッドで実行することでパネル表示で待たされないようにしてある。

 いずれにせよ、双方向バリデーションの合理化を追求すれば、必然的にCPUの負荷が増えるということではある。しかしそれはぜんぜんかまわない。機械が厳密にやれることを人手でチマチマやってもしょうがないからだ。

|

« 企業システムの基本構造を理解しよう | トップページ | DBMSの方言を開発基盤で吸収する »

コメント

コメントを書く



(ウェブ上には掲載しません)




トラックバック


この記事へのトラックバック一覧です: バリデーションの双方向性問題を解く:

« 企業システムの基本構造を理解しよう | トップページ | DBMSの方言を開発基盤で吸収する »