japan.internet.comThe Internet & IT Network
RSS
  • ニュース
  • コラム
  • リサーチ
  • ヘッドライン
  • 特集
  • ブログ
  • プレスリリース
  • 専門チャンネル
  • イベント
  • ランキング
  • ニュースメール
2008年9月5日
文字サイズ文字サイズ小文字サイズ中文字サイズ大
デベロッパー2006年12月12日 10:00

実環境におけるデータベース更新の競合に対処する

海外海外internet.com発の記事
  • このエントリーを含むはてなブックマーク
  • この記事をクリップ!
  • Buzzurlにブックマーク
  • Yahoo!ブックマークに登録
  • newsing it!

はじめに

 データベース同時実行時の競合を検出したり、それに対処したりする方法については、数多くの記事が書かれています。

 残念ながら、こうした記事と、そこに記されているソリューションのほとんどは、大きな欠陥を抱えています。いずれも、実際のデータとそのデータの使い方ではなく、技術的な問題とデータベースの実装に焦点が当てられているのです。本稿では、データベースの実装に焦点を当てることと、実環境のデータに焦点を当てることの違いを説明し、これらの同時実行時の問題を解決する方法の有効なアプローチをいくつか紹介します。

データベース同時実行時の競合とは何か

 初めに、データベース同時実行時の競合とは何か、なぜそれを解決する必要があるかを簡単に説明しましょう。

 データベースアプリケーションの大半はマルチユーザーアプリケーションです。つまり、任意の時点で、複数のユーザー/プロセスが同じデータベースとの間で、読み取り/書き込みを行うことが予想されます。複数のユーザー/プロセスが同じデータベース内のデータを更新しているとすれば、2人/2つの異なるユーザー/プロセスが、同時に同一のデータの更新を試みることは時間の問題です。通常の更新サイクルは次の一連のアクションから成り立ちます。

  • メモリにデータを読み込む
  • メモリ内のデータを更新する
  • データをデータベースに書き込む

 従って、2人のユーザーがメモリに同じデータを読み込むこともあるわけです。そのデータをユーザー2が更新して変更をデータベースに書き込む前に、ユーザー1が同じことをするかもしれません。ここで同時実行制御の競合が発生します。なぜなら、ユーザー2が持っているデータは、ユーザー1がデータベースにデータを書き込む前に読み取ったものだからです。ユーザー2がデータをデータベースに書き込めば、ユーザー1が加えた変更を上書きすることになり、結果的にユーザー1の変更が失われてしまいます。基本的には、最後に変更を保存した人の勝ちになります。先に保存した人の変更は、その後に保存された変更によって上書きされてしまいます。

 この種のデータベース同時実行の問題は、ユーザー間でも、自動化されたプロセス間でも、またその両者の間でも発生する可能性があります。ユーザー間の方が、読み取り/更新/書き込みサイクルの時間が長いので同時実行の問題が発生する確率が高くなります。しかし、自動化されたプロセス間でも同じ問題が発生する可能性はあり、通常はこちらの方が解決が困難です。ユーザーによる更新の場合は、ユーザーに何をしたいか(他のユーザーが加えた変更を上書きしたいか)を質問してその答えを得ることができますが、プロセスの場合は、すべてのアクションを完全に自動化する必要があるからです。

現在のアプローチ

 まず、データベースの同時実行問題の解決策として一般的に言われていることと行われていることを説明しましょう。通常、この問題の解決は、2つの基本的なアプローチに分けることができます。

  1. ペシミスティック(悲観的)な同時実行制御
  2. オプティミスティック(楽観的)な同時実行制御

 問題を処理するために、この2つの異なる選択肢について簡単に説明します。ここでは、問題を明確にすることだけに焦点を当て、問題処理の適用範囲とその性質についての説明は一切省略します。

ペシミスティックな同時実行制御

 ペシミスティックな同時実行制御では、アプリケーションがデータを変更する前にユーザー/プロセスにあるアクションを要求することで競合を防止します。この「アクション」は複数の要素から構成することができますが、通常は、データベース内のデータをロックし、それによって別のユーザーが同じロックを保持できないようにします。

長所と短所

 長所:

  • 実装が簡単 -- データベースサーバーはロックメカニズムをサポートし実施しているため、ペシミスティックな同時実行制御の実装は非常に簡単です。ユーザーは変更を加える前にロックをかける必要があるので、データベースサーバーは変更前に競合があることをユーザーに通知します。
  • 非常に安全 -- データベースサーバーでのロックの実装は非常に信頼性が高いため、絶対にロックを無視してデータを変更できないことが保証されます。

 短所:

  • スケーラビリティに劣る -- データベース内のデータをロックするには、データベースとの接続を確立する必要があります。つまり、すべてのユーザーが少なくとも1つのデータベース接続を確立しなければならず、それだけ多くのリソースとライセンスが必要になります。また、使っているデータベースサーバーが古い場合は、ロックによって他のユーザーがデータの読み取りすらできなくなる可能性があります。
  • デッドロックが起こりやすい -- 2人のユーザーがどちらもデータAとBを変更したいとします。ユーザー1は最初にAをロックし、ユーザー2は最初にBをロックします。2人とも、もう一方のデータをロックしたいのですが、既にロック済みであるため、ロックをかけることができません。2人ともデータが使用可能になるまで待つことにした場合、デッドロックが発生します。
  • ロックが長時間続く可能性がある -- ユーザーがデータを変更し始めたら、そのデータはユーザーが保存するまでロックされたままとなります。しかし、ユーザーが他のことに気を取られたり、変更を保存せずに会議に行ったとしても、データはロックされたままです。最初の変更がコミットされるまで、他の誰も変更を加えることができません。

 ペシミスティックなロックには、実際のデータベースロックではなくソフトロックを使うこともできます。この方法は、あるフィールドを更新することによって、ユーザーがデータを使用中でありデータが「ロック中」であることを示します。接続を確立する必要がないため、スケーラビリティの問題はなくなります。

 しかし、このアプローチには欠点があります。データベース側でロックを実施するのではないため、別のコードによってロックが無視される可能性があります。また、ロックを手動で解除しなければならないので、解除しないとデータは永遠にロックされたままになります。

 ペシミスティックなロックはそれなりに意味があるものの、欠点も多くあり、.NETアプリケーションではデータが分離されているため、あまりうまく活用できません。

オプティミスティックな同時実行制御

 オプティミスティックな同時実行制御では、ユーザーがデータをロックすることで同時実行問題の発生を防ぐのではなく、データベースに書き込むときにその問題を検出して解決します。通常、開発者はさまざまなアプローチからオプティミスティックな同時実行制御を行っています。例えば次のようなアプローチがあります。

  1. チェックを行わず、最後のものを優先する
  2. ユーザーが変更したフィールドを比較する
  3. すべてのフィールドを比較する
  4. 行バージョンを比較する

1.チェックを行わず、最後のものを優先する

 これは、サーバーが単に問題を無視するだけなので、本当のことを言えば同時実行制御のメカニズムではありません。最後にデータを更新するユーザーが、それまでのユーザーが加えた変更を上書きします。この場合のSQL UPDATEステートメントは、WHERE句で主キーのみをフィルタとして使用します。この種の同時実行制御はシングルユーザーアプリケーションにしか適していません。

2.ユーザーが変更したフィールドを比較する

 この場合は、更新処理の中で、ユーザーが変更したいデータとデータベース内のデータを比較し、ユーザーがデータを読み取ったときと同じデータであることが確認できた場合は変更をコミットします。最初に読み取ったデータとデータベース内のデータが異なる場合、サーバーはユーザーの変更をコミットせずに、ユーザーに警告を発行します。この場合のSQL UPDATEステートメントは、主キーと、古い値を持つ変更対象のフィールドと、WHERE句を使用します。

3.すべてのフィールドを比較する

 この種のオプティミスティックな同時実行制御のアプローチでは、更新をコミットする前に、変更されたフィールドだけでなく、すべてのフィールドをチェックします。これは手間のかけすぎのように思えますが、標準のADO.NETクラスの性質からすると、変更されたフィールドだけをチェックするよりも効果的です。ADO.NETデータクラスの特徴は、ユーザーが変更したフィールドだけではなく、すべてのフィールドを使ってSQL UPDATEステートメントを実行することです。つまり、現在のユーザーが変更しようとする特定のフィールドについては他に変更したユーザーがいなくても、別の列のフィールドを他のユーザーが変更している可能性があるということです。この場合のSQL UPDATEステートメントは、テーブル内のすべてのフィールドを含むSQL WHERE句を使用します。

4.行バージョンを比較する

 このアプローチでは、データに行バージョンフィールドという追加フィールドを用意します。このフィールドは、タイムスタンプフィールドとも言い、サーバーはデータを更新するたびにこのフィールドを変更します。この方法だと、サーバーはSQL UPDATEステートメント内の主キーと行バージョンフィールドについてのみフィルタ処理を行えばよいので、すべてのフィールドを比較する場合よりも処理が単純になり、時間も短縮されます。この方法はすべてのフィールドを比較する場合と同じ動作に見えるかもしれませんが、結果的には異なるため、注意が必要です。

 あるユーザーが現在の値ですべてのデータを更新するという更新処理を送信した場合も、データの行バージョンは更新されます。つまり、同じ行に何か変更を加えたい2番目のユーザーがいる場合、サーバーが行バージョンをチェックするアプローチでは競合が検出されますが、すべてのフィールドを比較するアプローチの場合は競合が検出されることはありません。

長所と短所

 長所:

  • スケーラビリティに優れている -- アプリケーションはデータベースと接続を確立しておく必要がないので、大人数のユーザーに対応することができます。
  • 実装が簡単 -- 特に、行バージョンフィールドを使う場合は簡単です。
  • デッドロックのおそれがほとんどない -- データベースサーバーはアプリケーションの動作を妨げるロックを持ち続けることがないため、デッドロックが発生するおそれもありません。唯一リスクがあるとすれば、アプリケーションロジックそのものの問題です。

 短所:

  • 安全性に劣る -- データベースは使用中のデータをロックすることができないため、不正なアプリケーションが行バージョンを無視してデータを構わずに更新してしまう可能性が残ります。
  • 単一行指向である -- すべての行を単一の作業単位として扱います。これが特に当てはまるのが、行バージョンフィールドです。

実環境での問題

 データベースの同時実行問題を詳しく説明している記事は他にもたくさんありますが、これらの記事には1つの共通する欠陥があります。それは、問題全体を、ユーザーの観点からではなく、データベースの観点から見て説明している点です。

 私が何を言いたいかお分かりでしょうか。これまで私が目にしてきたデータベースの同時実行の説明とソリューション案は、いつも1つのテーブル内の1行全体の同時実行時の競合と検出を焦点にしていました。実環境での更新は、複数の行、複数のテーブル、場合によっては複数のデータベースにまたがることもよくあります。ユーザーに対するデータの表示方法は、通常は更新対象の1つの行にマッピングされていません。

 以降では会計パッケージの例を取り上げて解説を続けます。ここで紹介する原則は、すべてではないものの、中規模から大規模のほとんどのアプリケーションに当てはまります。

 ユーザーが表示するデータは次のとおりです。

  • 単純な参照データ -- この種のデータは通常は1つの行にマッピングされます。例えば、適切なデータが入っているVATコード(注1)のリストがこれに相当します。
  • 複合的なアプリケーションデータ -- 通常、これは複数の行、またはテーブルにまたがるデータから成り立ちます。例えば、請求書や発注書などがこれに相当します。
  • 標準のアプリケーションデータ -- この種のデータは、通常はデータベース内の1つの行に格納されますが、ユーザーはこれを「関連性はあるが半分は異種であり、所有者が異なるデータの集合」と見なします。例えば、品目データがこれに相当します。アプリケーションは品目に関するすべてのデータを1つの行に保管するのに対し、ユーザーはデータの各部分を誰が「所有」しているかに基づいてデータを分割します。
  • 基本的な品目データ -- これは、すべてのユーザーが使うSKU(注2)と説明から成り立ちます。
  • 販売情報 -- この行には、販売価格、請求処理時に使うVAT情報、販売されたが出荷されていないユニットの数などが入ります。このデータはすべて営業部が所有および更新し、他のユーザーは読み取り専用となります。
  • 購買情報 -- この行には、優先供給業者、最高購入価格、発注したが入庫していない品目の数などが入ります。このデータは購買部が所有し更新します。
  • 在庫 -- この行には、在庫の数が入ります。物流部が在庫データを所有し、注文の処理に応じて出荷または入庫した品目の数を更新します。
用語解説
 注1:VATコード -- Value Added Taxの略称。付加価値税。世界80カ国に普及している間接税制度。ヨーロッパや海外諸国で生産の全過程において課せられる税金のこと。
 注2:SKU -- Stock Keeping Unitの略称。在庫管理を行う場合の管理単位のことで、SCMなどで在庫管理する際には、このSKUの単位で商品/製品を管理する。

単純な参照データ

 単純な参照データを処理するほとんどのアプリケーションでは、ユーザーが表示するデータセットはデータベース内の1行に正確にマッピングされるため、データの同時実行問題は標準的な方法でうまく対応できます。つまり、基本的なことはうまくカバーされるので、特に注意する必要はほとんどありません。ただし残念ながら、これはあまり変更されない種類のデータでもあるので、同時実行問題が発生する確率は非常に低くなります。

複合データ

 多くのアプリケーションでは、複合データをあまりうまく処理していません。2人のユーザーが同じ発注書を開き、各ユーザーがその発注書の別々の行を変更し、保存しようとしたとしましょう。技術的に言えば、各ユーザーはデータベース内の異なる行を変更しているので、競合は発生しません。しかし、ユーザーが表示しているのは1つの発注書であり、データベース内の別々の行ではないので、心配するとなれば、これは2人のユーザーが同じデータを変更するマルチユーザーによる競合です。

 ある程度までは、この問題は組織内の手続きに従って解決することができます。また、このようなシナリオが発生する確率は非常に小さいと主張する人もいるでしょう。そうかもしれませんが、遅かれ早かれ、発注書は、承認または出荷のために、営業部から別の部署に移動します。この時点で、組織の2つのまったく異なる部署が関わったことになります。一方の部署は明細を更新するのに対し、もう一方の部署はヘッダレコードを更新することでその発注の所有者になります。再び、厳密に言えば、2人のユーザーが更新するテーブルは異なるので競合はないのですが、実環境では、これはまさにマルチユーザーによる競合です。

 このようなシナリオを解決するには、個別の品目ではなく、データベース内の個々の行を全体として考える必要があります。アプリケーションがデータベースに発注を格納する方法を変更し、データを1つの行に格納することを考えます。SQL Server 2005のXMLフィールドを使えば、すべての明細をXML要素として格納することができます。ただし、このソリューションで同時実行問題は解決されるものの、このアプローチには欠点もたくさんあります。中でも特に目立つのが、XMLでの明細と、データベースのその他の部分(データベース自身では処理できずコードで処理しなければならない部分)との相関的な整合性チェックがないことです。

 複合データの同時実行問題を解決するためのもう1つのアプローチは、明細行を更新するときに発注書のヘッダの行バージョンをチェックすることです。このチェックの基本的な仕組みは次のとおりです。

  1. データベーストランザクションを開始します。
  2. 発注書のヘッダ行の行バージョンをチェックします。
  3. 発注書のヘッダ行を更新します。発注書のヘッダに実際には変更を加えていない場合でも、この更新によって行バージョンを更新します。
  4. 明細行をすべて更新、削除、または挿入します。
  5. トランザクションをコミットします。

 ここでの問題は、データベースが実際にはこのチェックの実施を支援できないことと、発注書のヘッダ行のバージョンを更新せずに発注明細だけを更新するプロセスがないことです。この問題を解決するには、発注明細テーブルで定義されたトリガを使って発注書のヘッダ行のバージョンを更新することです。

 このように、ヘッダ行のバージョンが確実に更新されるようにすることはできますが、これは、明細行のいずれかが更新された場合に、メモリ内の発注のヘッダ行を更新しなければならないことを意味します。繰り返しますが、これは理想的な状況ではありませんが、少なくとも非常に整合性があり、しかもテスト可能です。これをテストするには、同じ発注を2回変更します。2回目の変更は、1回目の変更を保存した直後に行います。2回目の変更を保存できた場合、メモリ内の行バージョンはデータベース内の行バージョンと同じになります。

標準のアプリケーションデータ

 ほとんどのソリューションは、どちらにしても、この標準のアプリケーションデータをあまり適切に処理していません。主な問題は、データベースと、データを処理するための.NETクラスが、両方とも1行全体を中心にしている点です。Visual Studio内部のツールと、SqlCommandBuilderなどのクラスは両方とも、SQL SELECTステートメントに基づき、SQL SELECTのすべてのフィールドに対してSQL UPDATEステートメントを生成します。これは、単純な参照データであれば、通常、ユーザーは行全体を更新することになるので問題ないのですが、標準のアプリケーションデータの場合はあまり意味がありません。

 欠点を見るために簡単な例を取り上げましょう。データベースに債務者情報が格納されているとします。この情報には、住所、現在の残高、および債務者の与信限度が含まれます。債務者からの支払いがあるたび(または新しい発注が発生するたび)に、アプリケーションが自動的に残高を更新します。ユーザーが残高を直接更新することはできません。基本的に、残高はパフォーマンス上の理由から、計算フィールドとして実際に用意します。

 顧客窓口部のユーザーが顧客の住所を変更しているときに、財務部の別のユーザーがその債務者の与信限度を変更するとします。厳密には、2人のユーザーが同時に同じ行を更新したいので、同時実行時の競合が発生します。しかし、顧客窓口部は住所データを所有しているのに対し、財務部は与信限度を所有しているので、手続き的に言えば競合はありません。しかし、それぞれのユーザーにデータのサブセットだけを提供してもこの問題を解決することはできません。財務部は弁済能力の査定を行うために債務者の住所を知らなければならないからです。財務部は債務者の住所を更新する必要はないので、必要なのは住所フィールドへの読み取りアクセス権だけです。同じことは、顧客窓口部にも当てはまります。顧客窓口部は債務者の与信限度を知る必要があるかもしれませんが、その情報の更新は行いません。

 この例の両方の変更を、すべての債務者情報を更新する権限を持つユーザーが行うとしても、これを同時実行時の競合と呼ぶことはまずありません。言うまでもなく、SQL SELECTステートメントから得たすべてのフィールドを単純に含むSQL UPDATEステートメントも、ここでは活躍しません。この問題は、各フィールドを別々のエンティティとして更新することで解決できます。これが、前述の問題を回避し解決する方法ですが、住所などの特定のフィールドを論理的にグループにまとめているなど、問題をあまりにも単純化しすぎています。

問題を解決する

 本当に必要なのは、読み取り専用フィールドの概念と、読み取り専用以外のフィールドを個別のSQL UPDATEにグループ化することです。このためには、CommandBuilderクラスのように、データ定義を入力として受け取り、SQL UPDATEコマンドオブジェクトのコレクションを生成してデータを更新するクラスが必要です。SQL UPDATEコマンドのコレクションでは、住所用のコマンドと、与信情報用のコマンドを分けることができます(現在のユーザーが両方の情報を更新する権限を持っていることを前提とします)。

 このコードではコレクションを反復処理し、ユーザーが更新しようとする個々のフィールドについて、古い値と新しい値を含んでいるパラメータを各コマンドに設定します。特定のコマンドで古い値と新しい値がすべて一致した場合は、言うまでもなくデータのその部分に変更はないので、コマンドを実行する必要はありません。すべてのSQL UPDATEコマンドの読み込みが終了すると、コードはこれらのコマンドを実行することができます。この場合、これらの更新を単一のトランザクションの一部としてラップする必要はありません。すべてのSQL UPDATEコマンドの実行が正常終了すれば、競合は発生せず、ユーザーは仕事を進めることができます。競合が発生しても、それはデータの特定のサブセットに限定されるので、アプリケーションは遅延することなくデータのその他の部分を更新できます。しかし、更新をグループ化するために、SQLの行バージョンを使うことはできません。代わりに、各コマンドのSQL WHERE句の主キーによって、一緒に更新するフィールドの完全なリストを取り込むことになります。

 次のMain()メソッドで、更新の仕組みを示す簡単な例を紹介します。この例では、ExtendedProperties列にフィールドのグループ化情報を格納しています。同じグループ名を持つ列は1つのSQL UPDATEコマンドにまとめられます。

’’’ <summary>

’’’ Test application to check database updates.
’’’ </summary>
’’’ <remarks></remarks>
Sub Main()
   Dim dta As New pubsDataSetTableAdapters.titlesTableAdapter
   Dim table As pubsDataSet.titlesDataTable
   Dim cb As New CommandBuilder

   ’ Load the data

   table = dta.GetData()

   ’ Configure the table
   cb.ConfigureDataTable(table)

   ’ Make a change to the data
   table.Item(0).price *= 1.1

   ’table.Item(0).title = "New title"
   ’ Update the database
   cb.UpdateTable(table)
   Console.WriteLine( _
      "Press any key to terminate the application.")
   Console.ReadKey()

End Sub
型指定されたデータセットとSQL UPDATEコマンド
 Visual Studio 2005で型指定されたデータセット(typed dataset)を生成する場合、既定ではオプティミスティックな同時実行制御が使用されます。問題のテーブルにタイムスタンプ列がある場合は、このタイムスタンプを使って行バージョンを比較します。問題のテーブルにタイムスタンプ列がない場合は、データセットデザイナはすべてのフィールドのチェックを追加して同時実行時のエラーを検出します。
 同時実行チェックのないSQL UPDATEコマンドが必要な場合は、このチェックをオフにすることができます。これは新しいSQL UPDATEコマンドを生成するときの設計時アクションです。オプティミスティックな同時実行制御チェックのオン/オフを切り替えるにはデータセットデザイナを使用します。
  1. pubsDataSet.xsdをダブルクリックします。
  2. titlesTableAdapter右クリックし、[Configure]を選択します。
  3. [Advanced Options]をクリックします。
  4. [Use optimistic concurrency]の選択を解除します。
  5. [OK]をクリックします。
  6. [Finish]をクリックします。
 以上のステップに従うと、同時実行チェックのないtitlesTableAdapterが再生成されます。
 [Advanced Option]ダイアログを見ると同時実行時の不一致を防ぐことができるように思えますが、実際には検出しかできません。同時実行時の不一致が検出されると、System.Data.DBConcurrencyExceptionがスローされます。不一致をキャッチして処理することは、開発者の役目であることに変わりありません。
型指定されたpubsDataSetを作成する
 サンプルコードでは、型指定されたデータセットを使ってSQL Server Pubsデータベースにアクセスしています。型指定されたデータセットを作成すれば、データベースから迅速、かつ簡単にデータを取得できます。次のステップに従ってpubsDataSetを作成します。
  1. Shift+Alt+Dキーを押して[Data Sources]ウィンドウを開くか、[Data]メニューから[Show Data Sources]を選択します。
  2. [Add New Data Source]をクリックします。
  3. [Database]を選択し[Next]をクリックします。
  4. 定義済みの既存の接続がある場合は、その接続を選択します。既存の接続がない場合は、[New Connection]をクリックして新しい接続を作成し、SQL ServerからPubsデータベースを選択します。
  5. [Next]をクリックします。
  6. オブジェクトのリストでTablesノードを展開します。
  7. titlesテーブルを選択します。
  8. データセット名がpubsDataSetであることを確認します。
  9. [Finish]をクリックします。
 これで、プロジェクト内にファイル「pubsDataSet.xsd」が表示されるはずです。このスキーマを作成すると、「pubsDataSet.Designer.vb」という分離コードのソースファイルも生成されます。ソリューションエクスプローラで[Show All Files]をクリックすると、このファイルが表示されます。このVisual Basicファイルには、Pubsのtitlesテーブル内のデータへの型指定されたアクセスを提供する数多くの生成されたクラスが含まれます。

 メインプログラムは、型指定されたテーブルアダプタを使ってPubsデータベースからtitlesテーブルを読み込みます。他の関数はすべて、メイン関数で作成されたCommandBuilderクラスの一部です。

 次のConfigureDataTable()関数で、各列はUpdateGroup拡張プロパティを受け取ります。これを更新中に使って、どのフィールドをグループ化するかを判別します。この情報は更新時には決定されません。なぜなら、フィールドのグループ化が異なるさまざまなビジネスオブジェクトで、同じテーブルを複数の目的に使う可能性があるからです。さらに、フィールドの中には、このサンプルコードが考慮に入れない読み取り専用のフィールドがある可能性もあります。

’’’ <summary>

’’’ Configure the columns into update groups.
’’’ </summary>
’’’ <param name="table">The table with columns.</param>
’’’ <remarks></remarks>
Public Sub ConfigureDataTable( _
   ByVal table As pubsDataSet.titlesDataTable)
   ’ Basic data about the book

   table.title_idColumn.ExtendedProperties("UpdateGroup") = "Book"
   table.titleColumn.ExtendedProperties("UpdateGroup") = "Book"
   table.typeColumn.ExtendedProperties("UpdateGroup") = "Book"

   table.notesColumn.ExtendedProperties("UpdateGroup") = "Book"
   table.pubdateColumn.ExtendedProperties("UpdateGroup") = "Book"

   ’ Financial data about the book
   table.pub_idColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"

   table.priceColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"
   table.advanceColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"
   table.royaltyColumn.ExtendedProperties("UpdateGroup") = _
      "Financial"

   ’ Sales information about the book
   table.ytd_salesColumn.ExtendedProperties("UpdateGroup") = _
      "Sales"
End Sub

 UpdateTable()関数(リスト1を参照)は、まずSQL UPDATEコマンドのコレクションを取得します。この例では、SQL INSERTコマンドとSQL DELETEコマンドの処理方法は通常と同じなので省略しました。

リスト1 UpdateTable()関数
’’’ <summary>
’’’ Sends all updates to the database
’’’ </summary>
’’’ <param name="table">The table with changes,</param>

’’’ <returns></returns>
’’’ <remarks>Just demo code.
’’’ Cannot execute as there is no connection and Insert/Delete
’’’ is not implemented.
’’’ </remarks>
Public Function UpdateTable(ByVal table As DataTable) As Boolean

   Dim updateCommands As List(Of SqlCommand)

   ’ Get a list of update commands to execute
   updateCommands = GetUpdateCommands(table)
   For Each row As DataRow In table.GetChanges(DataRowState.Modified).Rows()
      Select Case row.RowState
         Case DataRowState.Added
            ’ New row, do an database insert

         Case DataRowState.Deleted
            ’ Deleted row, do a database delete
         Case DataRowState.Modified
            ’ Changed row, do the required database updates
            For Each cmd As SqlCommand In updateCommands
               Dim hasChanges As Boolean = False

               For Each param As SqlParameter In _
                  cmd.Parameters()
                  ’ Populate all parameters
                  Dim fieldName As String

                  fieldName = param.ParameterName.Substring(3)
                  If param.ParameterName. StartsWith("old") Then
                     param.Value = row(fieldName, DataRowVersion.Original)
                  Else
                     param.Value = row(fieldName, DataRowVersion.Current)
                  End If

                  ’ Check if this field is changed
                  hasChanges = hasChanges OrElse Not row(fieldName, _
                     DataRowVersion.Original). Equals(row(fieldName, _
                     DataRowVersion.Current))
               Next
               If hasChanges Then Console.ForegroundColor = _
                  ConsoleColor.Yellow
                  Console.WriteLine( "Executing command:")
                  ’cmd.ExecuteScalar()

               Else
                  Console.ForegroundColor = ConsoleColor.Red
                  Console.WriteLine("Skipping command:")
               End If
               Console.WriteLine(cmd.CommandText)
               Console.WriteLine()
               Console.ResetColor()
            Next
      End Select

   Next
End Function

 次のコードは、GetUpdateCommands()関数がすべてのフィールドグループ内を反復処理し、フィールドグループごとに別々のSQL UPDATEコマンドを作成する方法を示しています。個々のコマンドは、すべて1つのコレクションにまとめられてコレクションが返されます。


’’’ <summary>
’’’ Build a collection of update commands for the table.
’’’ </summary>
’’’ <param name="table">
’’’ The table that needs to be updated.</param>
’’’ <returns>

’’’ A collection of SQLCommands for the update.</returns>
’’’ <remarks></remarks>
Private Function GetUpdateCommands(ByVal table As DataTable) _
   As List(Of SqlClient.SqlCommand)
   Dim groups As IDictionary(Of String, List(Of DataColumn))
   Dim cmds As List(Of SqlClient.SqlCommand)

   cmds = New List(Of SqlClient.SqlCommand)
   Console.WriteLine("Building update commands.")
   Console.WriteLine()

   ’ Split all columns into groups based upon the

   ’ UpdateGroup extended property.
   groups = SplitColumnIntoGroups(table)
   For Each group As List(Of DataColumn) In groups.Values
      Dim cmd As SqlCommand
      cmd = CreateUpdateCommand(table, group)
      cmds.Add(cmd)
      Console.WriteLine("Update command {0}:", cmds.Count)
      Console.WriteLine(cmd.CommandText)
      Console.WriteLine()
   Next

   Return cmds
End Function

 SplitColumnIntoGroups()関数(リスト2を参照)は、テーブル内のすべての列を取得して別々の更新グループに分けます。これは、読み取り専用の列と、通常は更新することができない主キーの列を除外するための絶好のポイントです。

リスト2 SplitColumnIntoGroups()関数
’’’ <summary>
’’’ Split all columns into groups based upon the UpdateGroup
’’’ extended property.
’’’ </summary>
’’’ <param name="table">

’’’ The table with columns to split.</param>
’’’ <returns>A dictionary with the groups of columns.</returns>
’’’ <remarks></remarks>
Private Function SplitColumnIntoGroups(ByVal table As DataTable) _
   As Dictionary(Of String, List(Of DataColumn))
   Dim groups As New Dictionary(Of String, List(Of DataColumn))

   For Each col As Data.DataColumn In table.Columns
      Dim updateGroup As String

      If col.ExtendedProperties.Contains("UpdateGroup") Then
         updateGroup = col.ExtendedProperties("UpdateGroup").ToString()
      Else
         updateGroup = ""

      End If
      If Not groups.ContainsKey(updateGroup) Then
         groups.Add(updateGroup, New List(Of DataColumn))
      End If

      groups(updateGroup).Add(col)
   Next
   Return groups
End Function

 リスト3CreateUpdateCommand()関数は、フィールドのグループごとに1つのSQL UPDATEコマンドを作成します。SQL WHERE句は、行の主キーと更新する必要があるフィールドから成り立ちます。フィールドが同じ値で上書きされても問題にはならないので、CreateUpdateCommand()関数は、各フィールドを古い値および新しい値と比較し、2人のユーザーによる同じ変更が競合と見なされないようにします。

リスト3 CreateUpdateCommand()関数
’’’ <summary>
’’’ Create a SqlCommand to update the field group.
’’’ </summary>
’’’ <param name="table">The table being updated.</param>

’’’ <param name="group">The field group.</param>
’’’ <returns>The SqlCommand to update the table.</returns>
’’’ <remarks></remarks>
Private Function CreateUpdateCommand(ByVal table As DataTable, _
   ByVal group As IEnumerable(Of DataColumn)) As SqlCommand

   ’ Build an update command for the group of columns

   Dim cmd As New Data.SqlClient.SqlCommand
   Dim sqlSet As New System.Text.StringBuilder()
   Dim sqlWhere As New System.Text.StringBuilder()
   For Each col As DataColumn In table.PrimaryKey
      If sqlWhere.Length > 0 Then

         sqlWhere.Append(" and ")
      End If
      sqlWhere.Append("([")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @org")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(" or [")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @new")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(")")
      cmd.Parameters.AddWithValue("old" + col.ColumnName, col.DataType)
      cmd.Parameters.AddWithValue("new" + col.ColumnName, col.DataType)
   Next

   For Each col As DataColumn In group
      If sqlSet.Length > 0 Then

         sqlSet.Append(", ")
      End If
      sqlSet.Append("[")
      sqlSet.Append(col.ColumnName)
      sqlSet.Append("] = @new")
      sqlSet.Append(col.ColumnName)
      If sqlWhere.Length > 0 Then

         sqlWhere.Append(" and ")
      End If
      sqlWhere.Append("([")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @org")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(" or [")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append("] = @new")
      sqlWhere.Append(col.ColumnName)
      sqlWhere.Append(")")
      If Not cmd.Parameters.Contains("old" + col.ColumnName) Then

         cmd.Parameters.AddWithValue("old" + col.ColumnName, _
            col.DataType)
      End If
      If Not cmd.Parameters.Contains("new" + col.ColumnName) Then

         cmd.Parameters.AddWithValue("new" + col.ColumnName, _
            col.DataType)
      End If
      Dim commandText As String

      commandText = "Update [{0}] Set {1} Where ({2})"
      cmd.CommandText = String.Format(commandText, _
         table.TableName, sqlSet.ToString(), sqlWhere.ToString())
   Next
   Return cmd
End Function

おわりに

 ここで紹介した手法は、すべての更新の同時実行問題に対応する完全なソリューションではありませんが、正しい方向に向かう一歩だと思います。この取り組みは現在進行形なので、この先、個々のケースに対処する最適な方法を見つける人も出てくるでしょう。

 このソリューションが、使いやすく、あまりテクノロジ指向ではない性質のアプリケーションの作成に役立つことを願います。

著者紹介

Maurice de Beijer(Maurice de Beijer)
フリーのソフトウェア開発者、ベータテスタ、そしてMicrosoftのMVP受賞者である。.NET、オブジェクト指向、Visual FoxProのほか、技術的に難しい問題の解決を専門とする。「The Problem Solver」というサイト(www.TheProblemSolver.nl)を運営している。
関連テーマ
最新トップニュース
データメーション
【データメーション】
OSについて気に入らないこと(9月5日)
ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」
【ベンチャー専門家の目利きブログ「なぜこの企業は伸びるのか?」】
「導入期〜成長期へ!一歩一歩と前進を目指す『Annoii(アノイ)』」/maka hou,Inc.(9月5日)
最新テクノロジーの意外な処方箋
【最新テクノロジーの意外な処方箋】
グリッドコンピューティング技術でETに遭遇(9月5日)
Graphic Design Forum
【Graphic Design Forum】
古い Emigre を探して (9月3日)
エンジニアの独り言
【エンジニアの独り言】
データをローカルに保存するWebアプリケーション(8月22日)
デスマーチからの脱却
【デスマーチからの脱却】
30min. iPhoneアプリリリース(8月18日)
最新ハイテク講座
最新ハイテク講座
なぜ勝った? 世界No.1シェアをつかんだ“Windows”(9月5日)
developer.com
developer.com
デザインパターンの使い方: Composite(9月5日)
最新アフィリエイト事例にみる成功の法則
最新アフィリエイト事例にみる成功の法則
コンバージョンレートを高めよう!(9月5日)
百式のネットビジネス研究
百式のネットビジネス研究
ガジェット購入時に将来の買取保証プランを提供する「TechForward」(9月5日)
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
ビデオリサーチインタラクティブ調査(月間インターネットオーディエンスデータ)(9月4日)
「IT の耳」
「IT の耳」
【書評】『検索にガンガンヒットさせる SEO の教科書』――SEO テクニックで効果的に PR する(9月4日)
検索エンジンマーケティング
検索エンジンマーケティング
果たしてモバイル SEO は必要なのか?(9月4日)
Eメールマーケティング事情
Eメールマーケティング事情
読者が迷惑メールと認識する時…(9月3日)
日本と韓国のインターネットビジネス最新動向調査
日本と韓国のインターネットビジネス最新動向調査
日本と韓国の動画サイト比較1―現状(9月3日)
SNSをビジネスに活用しよう
SNSをビジネスに活用しよう
「しまじろう」に学ぶ企業内コミュニティの活性化のポイント(9月2日)
海外のインターネットコムアメリカ韓国ドイツトルコ
Copyright 2008 Jupitermedia Corporation All Rights Reserved.http://www.internet.com/