オプティミスティック同時実行制御を実装する (C#)
複数のユーザーがデータを編集できる Web アプリケーションの場合、2 人のユーザーが同じデータを同時に編集するリスクがあります。 このチュートリアルでは、オプティミスティック同時実行制御を実装して、このリスクに対処します。
はじめに
データの表示のみがユーザーに許可されている Web アプリケーションや、1 人しかデータを変更できない Web アプリケーションの場合は、2 人のユーザーが同時に作業しながら、お互いの変更内容を誤って上書きしてしまうおそれはありません。 しかし、複数のユーザーがデータを更新または削除できる Web アプリケーションの場合、あるユーザーの変更が、同時に作業している別のユーザーの変更と競合する可能性があります。 コンカレンシー ポリシーが設定されていないと、2 人のユーザーが同時に 1 つのレコードを編集している場合、後から変更をコミットしたユーザーが、最初のユーザーの変更をオーバーライドすることになります。
たとえば、Jisun と Sam の 2 人のユーザーが、GridView コントロールを使用して、製品を更新および削除できるアプリケーションのページにアクセスしていたとします。 両方が同じタイミングで GridView の [編集] ボタンをクリックします。 Jisun は製品名を "Chai Tea" に変更し、[更新] ボタンをクリックします。 その結果、データベースに UPDATE
ステートメントが送信され、製品の "すべて" の更新可能フィールドが設定されます (Jisun が更新したのは ProductName
フィールドだけですが)。 この時点で、データベースには、この特定の製品に対して "Chai Tea" という値、"飲料" カテゴリ、"Exotic Liquids" サプライヤーなどが含まれます。 ただし、Sam の画面の GridView には、編集可能な GridView 行にまだ "Chai" という製品名が表示されています。 Jisun の変更がコミットされてから数秒後、Sam はカテゴリを "調味料" に更新し、[更新] をクリックします。 その結果、UPDATE
ステートメントがデータベースに送信されます。これにより、たとえば製品名は "Chai" に、CategoryID
は、対応する飲料カテゴリ ID に設定されます。 製品名に対する Jisun の変更は上書きされました。 図 1 は、この一連のイベントをグラフィカルに示しています。
図 1: 2 人のユーザーがレコードを同時に更新する場合、一方のユーザーの変更によって、もう一方の変更が上書きされることがある (クリックするとフルサイズの画像が表示されます)
同様に、2 人のユーザーが 1 つのページにアクセスしていると、あるユーザーがレコードを削除したとき、そのレコードを別のユーザーが更新している可能性があります。 あるいは、あるユーザーがページを読み込んでから、[削除] ボタンをクリックするまでの間に、別のユーザーがそのレコードの内容を変更することもあります。
使用できる同時実行制御戦略は 3 つあります。
- 何もしない - ユーザーが同じレコードを同時に変更している場合、最後のコミットを優先させます (既定の動作)
- オプティミスティック同時実行制御 - コンカレンシーの競合が発生することがありますが、このような競合が発生しない時間が大半を占めることを前提としています。このため、競合が発生した場合は、他のユーザーが同じデータを変更しているため変更を保存できない旨を、ただそのユーザーに通知します
- ペシミスティック同時実行制御 - コンカレンシーの競合が日常的に発生し、別のユーザーによる同時アクティビティが原因で変更が保存されなかったと言われることが許容されないことを前提としています。このため、あるユーザーがレコードの更新を開始したら、そのレコードをロックし、そのユーザーが変更をコミットするまで他のユーザーがそのレコードを編集または削除できないようにします
ここまでのチュートリアルではすべて、既定のコンカレンシー解決戦略を使用してきました。つまり、最後の書き込みが優先されました。 このチュートリアルでは、オプティミスティック同時実行制御を実装する方法について説明します。
Note
このチュートリアル シリーズでは、ペシミスティック同時実行制御の例については説明しません。 ペシミスティック同時実行制御はほとんど使用されません。こうしたロックは、適切に放棄されない場合、他のユーザーのデータ更新を妨げるためです。 たとえば、あるユーザーが編集のためにレコードをロックし、ロックを解除せずにその日を終了した場合、そのユーザーが戻って更新を完了するまで、他のユーザーはそのレコードを更新できません。 このため、ペシミスティック同時実行制御が使用される状況では、通常、タイムアウトが設定されており、それに到達すると、ロックが解除されます。 チケット販売 Web サイトでは、ユーザーが注文プロセスを完了している間、特定の座席が短時間ロックされますが、これはペシミスティック同時実行制御の例です。
手順 1: オプティミスティック同時実行制御の実装方法を確認する
オプティミスティック同時実行制御は、更新または削除されるレコードの値が、更新または削除プロセスの開始時と同じであることを確認することによって機能します。 たとえば、編集可能な GridView で [編集] ボタンをクリックすると、レコードの値がデータベースから読み取られ、TextBoxes やその他の Web コントロールに表示されます。 これらの元の値は GridView によって保存されます。 その後、ユーザーが変更を加えて [更新] ボタンをクリックすると、元の値と新しい値がビジネス ロジック層に送信され、その後、データ アクセス層に送信されます。 データ アクセス層は、ユーザーが編集を開始した元の値が、データベース内の値と同じ場合にのみレコードを更新する SQL ステートメントを発行する必要があります。 図 2 は、この一連のイベントを示しています。
図 2: 更新または削除を成功させるには、元の値が現在のデータベースの値と同じでなければならない (クリックするとフルサイズの画像が表示されます)
オプティミスティック同時実行制御を実装するには、さまざまな方法があります (オプションをいくつか簡単に確認するには、Peter A. Bromberg のオプティミスティック同時実行制御の更新ロジックに関するページを参照してください)。 ADO.NET 型指定された DataSet には、チェック ボックスをオンにするだけで構成できる実装が 1 つ用意されています。 型指定された DataSet で TableAdapter のオプティミスティック同時実行制御を有効にすると、TableAdapter の UPDATE
ステートメントと DELETE
ステートメントが拡張され、WHERE
句の元の値すべての比較が含められます。 たとえば、次の UPDATE
ステートメントは、現在のデータベース値が、GridView のレコードを更新するときに最初に取得された値と等しい場合にのみ、製品の名前と価格を更新します。 @ProductName
および @UnitPrice
パラメーターには、ユーザーが入力した新しい値が含まれます。一方、@original_ProductName
と @original_UnitPrice
には、[編集] ボタンがクリックされたときに GridView に最初に読み込まれた値が含まれます。
UPDATE Products SET
ProductName = @ProductName,
UnitPrice = @UnitPrice
WHERE
ProductID = @original_ProductID AND
ProductName = @original_ProductName AND
UnitPrice = @original_UnitPrice
Note
この UPDATE
ステートメントは、読みやすくするために簡略化されています。 実際には、WHERE
句の UnitPrice
チェックはより複雑になります。UnitPrice
には NULL
が含まれる可能性があり、NULL = NULL
かどうかのチェックは常に False を返すためです (代わりに、IS NULL
を使用する必要があります)。
基になる別の UPDATE
ステートメントを使用するだけでなく、オプティミスティック同時実行制御を使用するように TableAdapter を構成すると、その DB ダイレクト メソッドのシグネチャも変更されます。 最初のチュートリアル「データ アクセス層を作成する」を思い出してください。DB ダイレクト メソッドは、スカラー値のリストを入力パラメーターとして受け入れるメソッドでした (厳密に型指定された DataRow または DataTable インスタンスではありません)。 オプティミスティック同時実行制御を使用する場合、DB ダイレクト Update()
および Delete()
メソッドには、元の値の入力パラメーターも含まれます。 さらに、バッチ更新パターン (スカラー値ではなく DataRows と DataTables を受け入れる Update()
メソッド オーバーロード) を使用するための BLL 内のコードも変更する必要があります。
既存の DAL の TableAdapters を拡張してオプティミスティック同時実行制御を使用するのではなく (BLL を変更する必要があります)、代わりに、型指定された NorthwindOptimisticConcurrency
という名前の DataSet を新しく作成し、そこにオプティミスティック同時実行制御を使用する Products
TableAdapter を追加します。 その後、オプティミスティック同時実行制御 DAL をサポートするために適切な変更を加えた ProductsOptimisticConcurrencyBLL
ビジネス ロジック層クラスを作成します。 この基礎が整ったら、ASP.NET ページを作成する準備が整います。
手順 2: オプティミスティック同時実行制御をサポートするデータ アクセス層を作成する
新しい型指定された DataSet を作成するには、App_Code
フォルダー内の DAL
フォルダーを右クリックし、NorthwindOptimisticConcurrency
という名前の DataSet を新しく追加します。 最初のチュートリアルで説明したように、これにより、新しい TableAdapter が型指定された DataSet に追加され、TableAdapter 構成ウィザードが自動的に起動されます。 最初の画面では、接続先のデータベースを指定するように、つまり、Web.config
の NORTHWNDConnectionString
設定を使用して同じ Northwind データベースに接続するように求められます。
図 3: 同じ Northwind データベースに接続する (クリックするとフルサイズの画像が表示されます)
次に、データのクエリを実行する方法として、アドホック SQL ステートメント、新しいストアド プロシージャ、既存のストアド プロシージャのいずれかを選択するように求められます。 元の DAL でアドホック SQL クエリを使用したので、ここでもこのオプションを使用します。
図 4: アドホック SQL ステートメントを使用して取得するデータを指定する (クリックするとフルサイズの画像が表示されます)
次の画面で、製品情報の取得に使用する SQL クエリを入力します。 元の DAL の Products
TableAdapter に使用したものとまったく同じ SQL クエリを使用しましょう。これにより、すべての Product
列と、製品のサプライヤー名およびカテゴリ名が返されます。
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories
WHERE Categories.CategoryID = Products.CategoryID)
as CategoryName,
(SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID)
as SupplierName
FROM Products
図 5: 元の DAL で Products
TableAdapter から同じ SQL クエリを使用する (クリックするとフルサイズの画像が表示されます)
次の画面に進む前に、[詳細オプション] ボタンをクリックします。 この TableAdapter でオプティミスティック同時実行制御を使用するには、[オプティミスティック同時実行制御を使用する] チェック ボックスをオンにするだけです。
図 6: [オプティミスティック同時実行制御を使用する] チェック ボックスをオンにしてオプティミスティック同時実行制御を有効にする (クリックするとフルサイズの画像が表示されます)
最後に、TableAdapter が使用するデータ アクセス パターンでは、DataTable にデータが格納され、DataTable が返されることを指定します。また、DB ダイレクト メソッドを作成する必要があることも指定します。 "DataTable を返す" パターンのメソッド名を GetData から GetProducts に変更して、元の DAL で使用した名前付け規則を反映させます。
図 7: TableAdapter ですべてのデータ アクセス パターンが使用されるようにする (クリックするとフルサイズの画像が表示されます)
ウィザードが完了すると、DataSet デザイナーに、厳密に型指定された Products
DataTable と TableAdapter が追加されます。 DataTable の名前を Products
から ProductsOptimisticConcurrency
に変更します。これを行うには、DataTable のタイトル バーを右クリックし、コンテキスト メニューから [名前の変更] を選択します。
図 8: 型指定された DataSet に DataTable と TableAdapter が追加される (クリックするとフルサイズの画像が表示されます)
ProductsOptimisticConcurrency
TableAdapter (オプティミスティック同時実行制御を使用) と Products TableAdapter (その制御を使用していない) の間の UPDATE
クエリと DELETE
クエリの違いを確認するには、TableAdapter をクリックして、プロパティ ウィンドウに移動します。 DeleteCommand
および UpdateCommand
プロパティの CommandText
サブプロパティで、DAL の更新または削除関連のメソッドが呼び出されたときにデータベースに送信される実際の SQL 構文を確認できます。 ProductsOptimisticConcurrency
TableAdapter の場合、次の DELETE
ステートメントが使用されます。
DELETE FROM [Products]
WHERE (([ProductID] = @Original_ProductID)
AND ([ProductName] = @Original_ProductName)
AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
OR ([SupplierID] = @Original_SupplierID))
AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
OR ([CategoryID] = @Original_CategoryID))
AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
OR ([UnitPrice] = @Original_UnitPrice))
AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
OR ([UnitsInStock] = @Original_UnitsInStock))
AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
OR ([ReorderLevel] = @Original_ReorderLevel))
AND ([Discontinued] = @Original_Discontinued))
元の DAL の Product TableAdapter に対する DELETE
ステートメントは、はるかにシンプルです。
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
ご覧のように、オプティミスティック同時実行制御を使用する TableAdapter に対する DELETE
ステートメントの WHERE
句には、Product
テーブルの既存の列の各値と、GridView (または DetailsView あるいは FormView) が最後に設定された時点での元の値との比較が含まれます。 ProductID
、ProductName
、Discontinued
以外のすべてのフィールドに NULL
値が含まれる可能性があるため、WHERE
句 の NULL
値を正しく比較するために、追加のパラメーターとチェックが含まれます。
このチュートリアルでは、オプティミスティック同時実行制御対応 DataSet に他の DataTables は追加しません。ASP.NET ページでは、製品情報の更新と削除のみが提供されるためです。 ただし、ProductsOptimisticConcurrency
TableAdapter に GetProductByProductID(productID)
メソッドを追加する必要があります。
これを行うには、TableAdapter のタイトル バー (Fill
および GetProducts
メソッド名のすぐ上の領域) を右クリックし、コンテキスト メニューから [クエリの追加] を選択します。 これにより TableAdapter クエリの構成ウィザードが起動します。 TableAdapter の初期構成と同様、アドホック SQL ステートメントを使用して GetProductByProductID(productID)
メソッドの作成を選択します (図 4 を参照)。 GetProductByProductID(productID)
メソッドによって特定の製品に関する情報が返されるので、このクエリが、行を返す SELECT
クエリ型であることを指定します。
図 9: クエリ型を "行を返す SELECT
" としてマークする (クリックするとフルサイズの画像が表示されます)
次の画面で、SQL クエリを使用するように求められます。これには TableAdapter の既定のクエリが事前に読み込まれています。 図 10 に示すように、既存のクエリを拡張して WHERE ProductID = @ProductID
句を含めます。
図 10: 事前に読み込まれたクエリに WHERE
句を追加して特定の製品レコードを返す (クリックするとフルサイズの画像が表示されます)
最後に、生成されたメソッド名を、FillByProductID
および GetProductByProductID
に変更します。
図 11: メソッドの名前を FillByProductID
および GetProductByProductID
に変更する (クリックするとフルサイズの画像が表示されます)
このウィザードが完了すると、TableAdapter には、データを取得するためのメソッドが 2 つ含まれるようになります。1 つは "すべて" の製品を返す GetProducts()
、もう 1 つは指定された製品を返す GetProductByProductID(productID)
です。
手順 3: オプティミスティック同時実行制御対応 DAL に対してビジネス ロジック層を作成する
既存の ProductsBLL
クラスには、バッチ更新パターンと DB ダイレクト パターンの両方を使用する例があります。 AddProduct
メソッドと UpdateProduct
オーバーロードは両方ともバッチ更新パターンを使用して、ProductRow
インスタンスを TableAdapter の Update メソッドに渡します。 一方、DeleteProduct
メソッドは、DB ダイレクト パターンを使用して、TableAdapter の Delete(productID)
メソッドを呼び出します。
新しい ProductsOptimisticConcurrency
TableAdapter では、DB ダイレクト メソッドで元の値も渡す必要があります。 たとえば、Delete
メソッドには、10 個の入力パラメーターとして元の ProductID
、ProductName
、SupplierID
、CategoryID
、QuantityPerUnit
、UnitPrice
、UnitsInStock
、UnitsOnOrder
、ReorderLevel
、Discontinued
が必要です。 これらの追加の入力パラメーターの値は、データベースに送信される DELETE
ステートメントの WHERE
句で使用され、データベースの現在の値が元の値にマップされている場合にのみ、指定されたレコードを削除します。
バッチ更新パターンで使用される TableAdapter の Update
メソッドのメソッド シグネチャは変更されていませんが、元の値と新しい値を記録するのに必要なコードは変更されています。 このため、オプティミスティック同時実行制御対応 DAL は、既存の ProductsBLL
クラスで使用するのではなく、新しい DAL を操作するための新しいビジネス ロジック層クラスを作成しましょう。
ProductsOptimisticConcurrencyBLL
という名前のクラスを、App_Code
フォルダー内の BLL
フォルダーに追加します。
図 12: ProductsOptimisticConcurrencyBLL
クラスを BLL フォルダーに追加する
次に、ProductsOptimisticConcurrencyBLL
クラスに次のコードを追加します。
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
protected ProductsOptimisticConcurrencyTableAdapter Adapter
{
get
{
if (_productsAdapter == null)
_productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
return _productsAdapter;
}
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, true)]
public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
{
return Adapter.GetProducts();
}
}
クラス宣言の先頭の using NorthwindOptimisticConcurrencyTableAdapters
ステートメントに注意してください。 NorthwindOptimisticConcurrencyTableAdapters
名前空間には、DAL のメソッドを提供する ProductsOptimisticConcurrencyTableAdapter
クラスが含まれています。 また、クラス宣言の前には System.ComponentModel.DataObject
属性があります。この属性は、Visual Studio に対して、ObjectDataSource ウィザードのドロップダウン リストにこのクラスを含めるよう指示します。
ProductsOptimisticConcurrencyBLL
の Adapter
プロパティは、ProductsOptimisticConcurrencyTableAdapter
クラスのインスタンスにすばやくアクセスできるようにします。また、これは元の BLL クラス (ProductsBLL
、CategoriesBLL
など) で使用されるパターンに従います。 最後に、GetProducts()
メソッドは、DAL の GetProducts()
メソッドを単純に呼び出し、データベース内の製品レコードごとに ProductsOptimisticConcurrencyRow
インスタンスが設定された ProductsOptimisticConcurrencyDataTable
オブジェクトを返します。
オプティミスティック同時実行制御で DB ダイレクト パターンを使用して製品を削除する
オプティミスティック同時実行制御を使用する DAL に対して DB ダイレクト パターンを使用する場合は、メソッドに新しい値と元の値を渡す必要があります。 削除する場合、新しい値は存在しないため、渡す必要があるのは元の値のみです。 BLL では、元のパラメーターすべてを入力パラメーターとして受け入れる必要があります。 ProductsOptimisticConcurrencyBLL
クラス内の DeleteProduct
メソッドで DB ダイレクト メソッドが使用されるようにしてみましょう。 つまり、このメソッドは、次のコードに示すように、10 個すべての製品データ フィールドを入力パラメーターとして取り込み、DAL に渡す必要があります。
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
(int original_productID, string original_productName,
int? original_supplierID, int? original_categoryID,
string original_quantityPerUnit, decimal? original_unitPrice,
short? original_unitsInStock, short? original_unitsOnOrder,
short? original_reorderLevel, bool original_discontinued)
{
int rowsAffected = Adapter.Delete(original_productID,
original_productName,
original_supplierID,
original_categoryID,
original_quantityPerUnit,
original_unitPrice,
original_unitsInStock,
original_unitsOnOrder,
original_reorderLevel,
original_discontinued);
// Return true if precisely one row was deleted, otherwise false
return rowsAffected == 1;
}
元の値、つまり GridView (または DetailsView あるいは FormView) に最後に読み込まれた値と、ユーザーが [削除] ボタンをクリックしたときのデータベースの値と異なる場合、WHERE
句はどのデータベース レコードとも一致せず、レコードへの影響はありません。 そのため、TableAdapter の Delete
メソッドは 0
を返し、BLL の DeleteProduct
メソッドは false
を返します。
オプティミスティック同時実行制御でバッチ更新パターンを使用して製品を更新する
前述のように、バッチ更新パターンに対する TableAdapter の Update
メソッドのメソッド シグネチャは、オプティミスティック同時実行制御が使用されているかどうかに関係なく同じです。 つまり、Update
メソッドでは、DataRow、DataRows の配列、DataTable、または型指定された DataSet が必要です。 元の値を指定するための追加の入力パラメーターはありません。 これが可能なのは、DataTable が、その DataRow の元の値と変更された値を追跡しているためです。 DAL がその UPDATE
ステートメントを発行すると、@original_ColumnName
パラメーターには DataRow の元の値が設定され、@ColumnName
パラメーターには DataRow の変更された値が設定されます。
ProductsBLL
クラス (元の非オプティミスティック同時実行制御 DAL を使用) で、バッチ更新パターンを使用して製品情報を更新する場合、コードでは、次の一連のイベントが実行されます。
- TableAdapter の
GetProductByProductID(productID)
メソッドを使用して、現在のデータベース製品情報をProductRow
インスタンスに読み込みます - 手順 1 の
ProductRow
インスタンスに新しい値を割り当てます - TableAdapter の
Update
メソッドを呼び出して、ProductRow
インスタンスを渡します
ただし、この一連の手順では、オプティミスティック同時実行制御が正しくサポートされません。これは、手順 1 で設定された ProductRow
はデータベースから直接設定されるためです。つまり、DataRow によって使用される元の値は、データベースに現在存在する値です。編集プロセスの開始時に GridView にバインドされた値ではありません。 代わりに、オプティミスティック同時実行制御対応 DAL を使用する場合は、次の手順を使用するように UpdateProduct
メソッド オーバーロードを変更する必要があります。
- TableAdapter の
GetProductByProductID(productID)
メソッドを使用して、現在のデータベース製品情報をProductsOptimisticConcurrencyRow
インスタンスに読み込みます - 手順 1 の
ProductsOptimisticConcurrencyRow
インスタンスに "元の" 値を割り当てます ProductsOptimisticConcurrencyRow
インスタンスのAcceptChanges()
メソッドを呼び出し、DataRow に現在の値が "元の" 値であることを指示しますProductsOptimisticConcurrencyRow
インスタンスに "新しい" 値を割り当てます- TableAdapter の
Update
メソッドを呼び出して、ProductsOptimisticConcurrencyRow
インスタンスを渡します
手順 1 では、指定した製品レコードに対する、現在のデータベース値すべてを読み取ります。 この手順は、"すべて" の製品列を更新する UpdateProduct
オーバーロードでは不要ですが (これらの値は手順 2 で上書きされるため)、列の値のサブセットのみが入力パラメーターとして渡されるオーバーロードには不可欠です。 元の値が ProductsOptimisticConcurrencyRow
インスタンスに割り当てられると、AcceptChanges()
メソッドが呼び出され、現在の DataRow 値が、UPDATE
ステートメント内の @original_ColumnName
パラメーターで使用される元の値としてマークされます。 次に、新しいパラメーター値が ProductsOptimisticConcurrencyRow
に割り当てられ、最後に、Update
メソッドが呼び出され、DataRow が渡されます。
次のコードは、すべての製品データ フィールドを入力パラメーターとして受け入れる UpdateProduct
オーバーロードを示しています。 ここでは示されていませんが、このチュートリアルのダウンロードに含まれる ProductsOptimisticConcurrencyBLL
クラスには、入力パラメーターとして製品の名前と価格のみを受け入れる UpdateProduct
オーバーロードも含まれています。
protected void AssignAllProductValues
(NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
string productName, int? supplierID, int? categoryID, string quantityPerUnit,
decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
short? reorderLevel, bool discontinued)
{
product.ProductName = productName;
if (supplierID == null)
product.SetSupplierIDNull();
else
product.SupplierID = supplierID.Value;
if (categoryID == null)
product.SetCategoryIDNull();
else
product.CategoryID = categoryID.Value;
if (quantityPerUnit == null)
product.SetQuantityPerUnitNull();
else
product.QuantityPerUnit = quantityPerUnit;
if (unitPrice == null)
product.SetUnitPriceNull();
else
product.UnitPrice = unitPrice.Value;
if (unitsInStock == null)
product.SetUnitsInStockNull();
else
product.UnitsInStock = unitsInStock.Value;
if (unitsOnOrder == null)
product.SetUnitsOnOrderNull();
else
product.UnitsOnOrder = unitsOnOrder.Value;
if (reorderLevel == null)
product.SetReorderLevelNull();
else
product.ReorderLevel = reorderLevel.Value;
product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
// new parameter values
string productName, int? supplierID, int? categoryID, string quantityPerUnit,
decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
short? reorderLevel, bool discontinued, int productID,
// original parameter values
string original_productName, int? original_supplierID, int? original_categoryID,
string original_quantityPerUnit, decimal? original_unitPrice,
short? original_unitsInStock, short? original_unitsOnOrder,
short? original_reorderLevel, bool original_discontinued,
int original_productID)
{
// STEP 1: Read in the current database product information
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
Adapter.GetProductByProductID(original_productID);
if (products.Count == 0)
// no matching record found, return false
return false;
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0];
// STEP 2: Assign the original values to the product instance
AssignAllProductValues(product, original_productName, original_supplierID,
original_categoryID, original_quantityPerUnit, original_unitPrice,
original_unitsInStock, original_unitsOnOrder, original_reorderLevel,
original_discontinued);
// STEP 3: Accept the changes
product.AcceptChanges();
// STEP 4: Assign the new values to the product instance
AssignAllProductValues(product, productName, supplierID, categoryID,
quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
discontinued);
// STEP 5: Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
手順 4: ASP.NET ページから BLL メソッドに元の値と新しい値を渡す
DAL と BLL が完了したら、あとは、システムに組み込まれているオプティミスティック同時実行制御ロジックを利用できる ASP.NET ページを作成するだけです。 具体的には、データ Web コントロール (GridView、DetailsView、または FormView) は、その元の値を覚えている必要があります。また、ObjectDataSource は、両方の値セットをビジネス ロジック層に渡す必要があります。 さらに、ASP.NET ページは、コンカレンシー違反を適切に処理するように構成しなければなりません。
まず、EditInsertDelete
フォルダー内の OptimisticConcurrency.aspx
ページを開き、GridView をデザイナーに追加し、その ID
プロパティを ProductsGrid
に設定します。 GridView のスマート タグから、ProductsOptimisticConcurrencyDataSource
という名前の新しい ObjectDataSource を作成します。 この ObjectDataSource は、オプティミスティック同時実行制御をサポートする DAL を使うように設定したいため、ProductsOptimisticConcurrencyBLL
オブジェクトを使用するように構成します。
図 13: ProductsOptimisticConcurrencyBLL
オブジェクトを使用するように ObjectDataSource を設定する (クリックするとフルサイズの画像が表示されます)
ウィザードのドロップダウン リストから GetProducts
、UpdateProduct
、および DeleteProduct
メソッドを選択します。 UpdateProduct メソッドの場合は、製品のすべてのデータ フィールドを受け入れるオーバーロードを使用します。
ObjectDataSource コントロールのプロパティを構成する
ウィザードが完了すると、ObjectDataSource の宣言型マークアップは次のようになります。
<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
UpdateMethod="UpdateProduct">
<DeleteParameters>
<asp:Parameter Name="original_productID" Type="Int32" />
<asp:Parameter Name="original_productName" Type="String" />
<asp:Parameter Name="original_supplierID" Type="Int32" />
<asp:Parameter Name="original_categoryID" Type="Int32" />
<asp:Parameter Name="original_quantityPerUnit" Type="String" />
<asp:Parameter Name="original_unitPrice" Type="Decimal" />
<asp:Parameter Name="original_unitsInStock" Type="Int16" />
<asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
<asp:Parameter Name="original_reorderLevel" Type="Int16" />
<asp:Parameter Name="original_discontinued" Type="Boolean" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="supplierID" Type="Int32" />
<asp:Parameter Name="categoryID" Type="Int32" />
<asp:Parameter Name="quantityPerUnit" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="unitsInStock" Type="Int16" />
<asp:Parameter Name="unitsOnOrder" Type="Int16" />
<asp:Parameter Name="reorderLevel" Type="Int16" />
<asp:Parameter Name="discontinued" Type="Boolean" />
<asp:Parameter Name="productID" Type="Int32" />
<asp:Parameter Name="original_productName" Type="String" />
<asp:Parameter Name="original_supplierID" Type="Int32" />
<asp:Parameter Name="original_categoryID" Type="Int32" />
<asp:Parameter Name="original_quantityPerUnit" Type="String" />
<asp:Parameter Name="original_unitPrice" Type="Decimal" />
<asp:Parameter Name="original_unitsInStock" Type="Int16" />
<asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
<asp:Parameter Name="original_reorderLevel" Type="Int16" />
<asp:Parameter Name="original_discontinued" Type="Boolean" />
<asp:Parameter Name="original_productID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
ご覧のように、DeleteParameters
コレクションには、ProductsOptimisticConcurrencyBLL
クラスの DeleteProduct
メソッド内の 10 個の入力パラメーターごとに Parameter
インスタンスが含まれています。 同様に、UpdateParameters
コレクションには、UpdateProduct
の入力パラメーターごとに Parameter
インスタンスが含まれています。
データの変更に関連する以前のチュートリアルでは、この時点で ObjectDataSource の OldValuesParameterFormatString
プロパティを削除しました。このプロパティによって、BLL メソッドが新しい値だけでなく、古い (または元の) 値が渡されることを想定していることが示されるためです。 さらに、このプロパティ値は、元の値の入力パラメーター名を示します。 元の値を BLL に渡しているため、このプロパティは削除 "しない" でください。
Note
OldValuesParameterFormatString
プロパティの値は、元の値を想定する BLL の入力パラメーター名にマップする必要があります。 これらのパラメーターには original_productName
、original_supplierID
などの名前を付けたので、OldValuesParameterFormatString
プロパティ値は original_{0}
のままにできます。 ただし、BLL メソッドの入力パラメーターに old_productName
、old_supplierID
などの名前がついている場合は、OldValuesParameterFormatString
プロパティは old_{0}
に更新する必要があります。
ObjectDataSource によって元の値が BLL メソッドに正しく渡されるようにするには、最後にもう 1 つプロパティ設定を行う必要があります。 ObjectDataSource には ConflictDetection プロパティがあり、次の 2 つの値のいずれかに割り当てることができます。
OverwriteChanges
- 既定値。元の値を BLL メソッドの元の入力パラメーターに送信しませんCompareAllValues
- 元の値を BLL メソッドに送信します。オプティミスティック同時実行制御を使用する場合は、このオプションを選択します
ConflictDetection
プロパティを CompareAllValues
に設定します。
GridView のプロパティとフィールドを構成する
ObjectDataSource のプロパティが正しく設定されたので、GridView の設定に注目しましょう。 最初に、GridView で編集と削除をサポートする必要があるため、GridView のスマート タグから [編集を有効にする] と [削除を有効にする] のチェック ボックスをオンにします。 これにより CommandField が追加され、ShowEditButton
と ShowDeleteButton
の両方が true
に設定されます。
ProductsOptimisticConcurrencyDataSource
ObjectDataSource にバインドされている場合、GridView には、製品のデータ フィールドごとにフィールドが含まれます。 このような GridView は編集できますが、ユーザー エクスペリエンスは決して許容できるものではありません。 CategoryID
および SupplierID
BoundFields は TextBoxes としてレンダリングされ、ユーザーは適切なカテゴリとサプライヤーを ID 番号として入力する必要があります。 数値フィールドは書式設定できず、製品の名前が指定されていること、また単価、在庫数、受注単位、標準在庫数の値が適切な数値で、0 以上であることを確認する検証コントロールもありません。
「編集および挿入インターフェイスに検証コントロールを追加する」チュートリアルと「データ変更インターフェイスをカスタマイズする」チュートリアルで説明したように、ユーザー インターフェイスをカスタマイズするには、BoundFields を TemplateFields に置き換えます。 この GridView とその編集インターフェイスは次の方法で変更しました。
ProductID
、SupplierName
、およびCategoryName
BoundFields を削除しましたProductName
BoundField を TemplateField に変換し、RequiredFieldValidation コントロールを追加しました。CategoryID
およびSupplierID
BoundFields を TemplateFields に変換し、TextBoxes ではなく DropDownLists を使用するように編集インターフェイスを調整しました。 これらの TemplateFields のItemTemplates
には、CategoryName
およびSupplierName
データ フィールドが表示されます。UnitPrice
、UnitsInStock
、UnitsOnOrder
、ReorderLevel
BoundFields を TemplateFields に変換し、CompareValidator コントロールを追加しました。
これらのタスクを実行する方法については、これまでのチュートリアルで既に確認しました。ここでは最後の宣言構文の一覧を示すだけにとどめ、実装は練習として残しておきます。
<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
OnRowUpdated="ProductsGrid_RowUpdated">
<Columns>
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
<asp:TemplateField HeaderText="Product" SortExpression="ProductName">
<EditItemTemplate>
<asp:TextBox ID="EditProductName" runat="server"
Text='<%# Bind("ProductName") %>'></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1"
ControlToValidate="EditProductName"
ErrorMessage="You must enter a product name."
runat="server">*</asp:RequiredFieldValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Bind("ProductName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
<EditItemTemplate>
<asp:DropDownList ID="EditCategoryID" runat="server"
DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
DataTextField="CategoryName" DataValueField="CategoryID"
SelectedValue='<%# Bind("CategoryID") %>'>
<asp:ListItem Value=">(None)</asp:ListItem>
</asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
runat="server" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label2" runat="server"
Text='<%# Bind("CategoryName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
<EditItemTemplate>
<asp:DropDownList ID="EditSuppliersID" runat="server"
DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
DataTextField="CompanyName" DataValueField="SupplierID"
SelectedValue='<%# Bind("SupplierID") %>'>
<asp:ListItem Value=">(None)</asp:ListItem>
</asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
runat="server" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
</asp:ObjectDataSource>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label3" runat="server"
Text='<%# Bind("SupplierName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
SortExpression="QuantityPerUnit" />
<asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
<EditItemTemplate>
<asp:TextBox ID="EditUnitPrice" runat="server"
Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
<asp:CompareValidator ID="CompareValidator1" runat="server"
ControlToValidate="EditUnitPrice"
ErrorMessage="Unit price must be a valid currency value without the
currency symbol and must have a value greater than or equal to zero."
Operator="GreaterThanEqual" Type="Currency"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label4" runat="server"
Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
<EditItemTemplate>
<asp:TextBox ID="EditUnitsInStock" runat="server"
Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
<asp:CompareValidator ID="CompareValidator2" runat="server"
ControlToValidate="EditUnitsInStock"
ErrorMessage="Units in stock must be a valid number
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label5" runat="server"
Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
<EditItemTemplate>
<asp:TextBox ID="EditUnitsOnOrder" runat="server"
Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
<asp:CompareValidator ID="CompareValidator3" runat="server"
ControlToValidate="EditUnitsOnOrder"
ErrorMessage="Units on order must be a valid numeric value
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label6" runat="server"
Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
<EditItemTemplate>
<asp:TextBox ID="EditReorderLevel" runat="server"
Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
<asp:CompareValidator ID="CompareValidator4" runat="server"
ControlToValidate="EditReorderLevel"
ErrorMessage="Reorder level must be a valid numeric value
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label7" runat="server"
Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
SortExpression="Discontinued" />
</Columns>
</asp:GridView>
完全に動作する例になるまであと少しです。 しかし、やがて微妙な点がひそかに発生するようになり、それが問題を引き起こします。 さらに、コンカレンシー違反が発生したときにユーザーに警告するインターフェイスは、やはり必要です。
Note
データ Web コントロールが元の値を ObjectDataSource に正しく渡すには (これは、その後 BLL に渡されます)、GridView の EnableViewState
プロパティが true
(既定値) に設定されていることが重要です。 ビュー状態を無効にすると、元の値はポストバック時に失われます。
正しい元の値を ObjectDataSource に渡す
GridView の構成方法には問題がいくつかあります。 ObjectDataSource の ConflictDetection
プロパティが CompareAllValues
に設定されている場合 (当サイトと同様)、ObjectDataSource Update()
または Delete()
メソッドが GridView (または DetailsView あるいは FormView) によって呼び出されると、ObjectDataSource は、GridView の元の値を、その適切な Parameter
インスタンスにコピーしようとします。 このプロセスをグラフィカルに表現したものについては、図 2 を参照してください。
具体的には、GridView の元の値には、データが GridView にバインドされるたびに、双方向のデータバインド ステートメントの値が割り当てられます。 したがって、必要な元の値はすべて、双方向のデータ バインドを使用してキャプチャされること、また、変換可能な形式で提供されることが重要です。
これが重要である理由を確認するには、ブラウザーでページにアクセスしてください。 予想どおり、GridView には、製品の一覧と、それぞれの左端の列に [編集] ボタンと [削除] ボタンが表示されます。
図 14: GridView に製品の一覧が表示される (クリックするとフルサイズの画像が表示されます)
いずれかの製品の [削除] ボタンをクリックすると、FormatException
がスローされます。
図 15: 製品を削除しようとすると FormatException
が発生する (クリックするとフルサイズの画像が表示されます)
ObjectDataSource が元の UnitPrice
値を読み取ろうとすると、FormatException
が発生します。 ItemTemplate
の UnitPrice
は通貨 (<%# Bind("UnitPrice", "{0:C}") %>
) として書式設定されているため、これには通貨記号が含まれます ($19.95 など)。 ObjectDataSource がこの文字列を decimal
に変換しようとすると、FormatException
が発生します。 この問題を回避するためのオプションはいくつかあります。
ItemTemplate
から通貨書式を削除します。 つまり、<%# Bind("UnitPrice", "{0:C}") %>
ではなく、単に<%# Bind("UnitPrice") %>
を使用します。 この欠点は、価格が書式設定されなくなることです。ItemTemplate
でUnitPrice
を通貨として書式設定して表示しますが、Eval
キーワードを使ってこれを行います。Eval
が一方向のデータ バインドを実行することを思い出してください。 引き続き元の値に対してUnitPrice
値を提供する必要があるため、ItemTemplate
にはまだ双方向のデータバインド ステートメントが必要ですが、これは、Visible
プロパティがfalse
に設定されているラベル Web コントロールに配置することができます。 ItemTemplate では、次のマークアップを使用できます。
<ItemTemplate>
<asp:Label ID="DummyUnitPrice" runat="server"
Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
<asp:Label ID="Label4" runat="server"
Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
<%# Bind("UnitPrice") %>
を使用して、ItemTemplate
から通貨書式を削除します。 GridView のRowDataBound
イベント ハンドラーで、ラベル Web コントロールにプログラムによってアクセスします。そのコントロールでは、UnitPrice
値が表示され、そのText
プロパティが書式設定されたバージョンに設定されています。UnitPrice
の通貨としての書式設定をそのままにします。 GridView のRowDeleting
イベント ハンドラーで、既存の元のUnitPrice
値 ($19.95) を、Decimal.Parse
を使用して実際の 10 進値に置き換えます。 「ASP.NET ページで BLL レベルと DAL レベルの例外を処理する」チュートリアルでは、RowUpdating
イベント ハンドラーで同様の操作を行う方法について確認しました。
この例では、2 番目のアプローチを選択し、非表示のラベル Web コントロールを追加することにしました。その Text
プロパティは、書式設定されていない UnitPrice
値にバインドされている双方向のデータです。
この問題を解決したら、製品の [削除] ボタンをもう一度クリックしてみてください。 今回は ObjectDataSource が BLL の UpdateProduct
メソッドの呼び出そうとすると、InvalidOperationException
が発生します。
図 16: ObjectDataSource は、送信したい入力パラメーターを持つメソッドを見つけることができない (クリックするとフルサイズの画像が表示されます)
例外のメッセージを見ると、ObjectDataSource が original_CategoryName
と original_SupplierName
入力パラメーターを含む BLL DeleteProduct
メソッドを呼び出したいことは明らかです。 これは、CategoryID
および SupplierID
TemplateFields の ItemTemplate
には現在、CategoryName
および SupplierName
データ フィールドを含む双方向の Bind ステートメントが含まれているためです。 代わりに、Bind
および CategoryID
データ フィールドを含む SupplierID
ステートメントを含める必要があります。 これを実現するには、次に示すように、既存の Bindステートメントを Eval
ステートメントに置き換え、非表示のラベル コントロールを追加します。その Text
プロパティは、双方向のデータバインドを使用して、CategoryID
および SupplierID
データ フィールドにバインドされています。
<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
<EditItemTemplate>
...
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="DummyCategoryID" runat="server"
Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
<asp:Label ID="Label2" runat="server"
Text='<%# Eval("CategoryName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
<EditItemTemplate>
...
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="DummySupplierID" runat="server"
Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
<asp:Label ID="Label3" runat="server"
Text='<%# Eval("SupplierName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
これらの変更により、製品情報を正常に削除および編集できるようになりました。 手順 5 では、コンカレンシー違反が検出されていることを確認する方法について説明します。 しかし、とりあえずは少し時間をとってレコードをいくつか更新および削除し、1 人のユーザーに対する更新と削除が想定どおりに動作することを確認してください。
手順 5: オプティミスティック同時実行制御サポートをテストする
(データがやみくもに上書きされるのではなく) コンカレンシー違反が検出されていることを確認するには、このページに対して 2 つのブラウザー ウィンドウを開く必要があります。 両方のブラウザー インスタンスで、Chai の [編集] ボタンをクリックします。 次に、ブラウザーの 1 つで、名前を "Chai Tea" に変更し、[更新] をクリックします。 更新が成功し、GridView が編集前の状態に戻り、"Chai Tea" が新しい製品名として表示されます。
ただし、もう 一方のブラウザー ウィンドウ インスタンスでは、まだ製品名 TextBox に "Chai" と表示されています。 この 2 番目のブラウザー ウィンドウで、UnitPrice
を 25.00
に更新します。 オプティミスティック同時実行制御がサポートされていない場合、2 番目のブラウザー インスタンスで [更新] をクリックすると、製品名が "Chai" に戻り、最初のブラウザー インスタンスで行われた変更が上書きされます。 しかし、オプティミスティック同時実行制御が採用されている場合、2 番目のブラウザー インスタンスで [更新] ボタンをクリックすると、DBConcurrencyException が発生します。
図 17: コンカレンシー違反が検出されると DBConcurrencyException
がスローされる (クリックするとフルサイズの画像が表示されます)
DBConcurrencyException
は、DAL のバッチ更新パターンが使用されている場合にのみスローされます。 DB ダイレクト パターンは例外を発生させるのではなく、影響を受けた行がないことを示すだけです。 これを示すために、両方のブラウザー インスタンスの GridView を編集前の状態に戻します。 次に、最初のブラウザー インスタンスで、[編集] ボタンをクリックし、製品名を "Chai Tea" から "Chai" に戻して、[更新] をクリックします。 2 番目のブラウザー ウィンドウで、Chai の [削除] ボタンをクリックします。
[削除] をクリックすると、ページはポストバックされ、GridView は ObjectDataSource の Delete()
メソッドを呼び出し、ObjectDataSource は ProductsOptimisticConcurrencyBLL
クラスの DeleteProduct
メソッドを呼び出して元の値を渡します。 2 番目のブラウザー インスタンスの元の ProductName
値は "Chai Tea" であり、データベース内の現在の ProductName
値と一致しません。 そのため、データベースに対して発行された DELETE
ステートメントの影響を受ける行はゼロです。WHERE
句を満たすレコードがデータベースに存在しないためです。 DeleteProduct
メソッドは false
を返し、ObjectDataSource のデータは GridView に再バインドされます。
エンド ユーザーの観点から見た場合、2 番目のブラウザー ウィンドウで Chai Tea の [削除] ボタンをクリックすると、画面が点滅します。戻ったとき、製品はまだそこにありますが、今度は "Chai" (最初のブラウザー インスタンスによって行われた製品名の変更) として表示されます。 ユーザーが [削除] ボタンをもう一度クリックすると、GridView の元の ProductName
値 ("Chai") がデータベース内の値と一致するようになったため、削除は成功します。
どちらの場合も、ユーザー エクスペリエンスは理想的とは言えません。 バッチ更新パターンを使用する場合、当然 DBConcurrencyException
例外の肝心な詳細をユーザーには見せたくありません。 また、DB ダイレクト パターンを使用する場合の動作は、ユーザー コマンドが失敗したため、ややわかりにくくなっていますが、その理由を正確に示すものはありませんでした。
この 2 つの問題を解決するために、更新または削除が失敗した理由が示されているラベル Web コントロールをページに作成することができます。 バッチ更新パターンの場合、GridView のポストレベル イベント ハンドラーで DBConcurrencyException
例外が発生したかどうかを判断し、必要に応じて警告ラベルを表示できます。 DB ダイレクト メソッドの場合は、BLL メソッドの戻り値 (1 つの行が影響を受けた場合は true
、それ以外の場合は false
) を調べて、必要に応じて情報メッセージを表示できます。
手順 6: 情報メッセージを追加し、コンカレンシー違反が発生した場合に表示する
コンカレンシー違反が発生した場合の動作は、DAL のバッチ更新パターンと DB ダイレクト パターンのどちらが使用されたかによって異なります。 このチュートリアルでは、両方のパターンを使用します。バッチ更新パターンは更新に使用され、DB ダイレクト パターンは削除に使用されます。 まず、データを削除または更新しようとしたときにコンカレンシー違反が発生したことを説明する 2 つのラベル Web コントロールをページに追加しましょう。 ラベル コントロールの Visible
プロパティと EnableViewState
プロパティを false
に設定します。これにより、そのコントロールは、各ページ アクセスで非表示になります (Visible
プロパティがプログラムによって true
に設定されている特定のページ アクセスを除く)。
<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to delete has been modified by another user
since you last visited this page. Your delete was cancelled to allow
you to review the other user's changes and determine if you want to
continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to update has been modified by another user
since you started the update process. Your changes have been replaced
with the current values. Please review the existing values and make
any needed changes." />
Visible
、EnabledViewState
、Text
プロパティの設定のほか、CssClass
プロパティを Warning
に設定しました。これにより、ラベルが大きな赤、斜体、太字のフォントで表示されます。 この CSS Warning
クラスは、「挿入、更新、削除に関連付けられているイベントを調べる」チュートリアルで定義され、Styles.css に追加されました。
これらのラベルを追加すると、Visual Studio のデザイナーは図 18 のようになります。
図 18: 2 つのラベル コントロールがページに追加されている (クリックするとフルサイズの画像が表示されます)
これらのラベル Web コントロールが配置されたので、コンカレンシー違反がいつ発生したかを判断する方法を調べる準備ができました。この時点で、適切なラベルの Visible
プロパティを true
に設定して、情報メッセージを表示できます。
更新時にコンカレンシー違反を処理する
まず、バッチ更新パターンを使用するときにコンカレンシー違反を処理する方法を見てみましょう。 バッチ更新パターンでこのような違反が発生することで DBConcurrencyException
例外がスローされるため、更新プロセス中に DBConcurrencyException
例外が発生したかどうかを判断するコードを ASP.NET ページに追加する必要があります。 その場合は、ユーザーがレコードの編集を開始してから、[更新] ボタンをクリックするまでの間に、別のユーザーが同じデータを変更したため、変更が保存されなかったことを説明するメッセージを表示する必要があります。
「ASP.NET ページで BLL レベルと DAL レベルの例外を処理する」チュートリアルで確認したように、このような例外は、データ Web コントロールのポストレベル イベント ハンドラーで検出および抑制することができます。 そのため、DBConcurrencyException
例外がスローされたかどうかをチェックする GridView の RowUpdated
イベントに対して、イベント ハンドラーを作成する必要があります。 このイベント ハンドラーには、以下のイベント ハンドラー コードに示すように、更新プロセス中に発生したすべての例外への参照が渡されます。
protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
if (e.Exception != null && e.Exception.InnerException != null)
{
if (e.Exception.InnerException is System.Data.DBConcurrencyException)
{
// Display the warning message and note that the
// exception has been handled...
UpdateConflictMessage.Visible = true;
e.ExceptionHandled = true;
}
}
}
DBConcurrencyException
例外が発生した場合、このイベント ハンドラーは UpdateConflictMessage
ラベル コントロールを表示して、例外が処理されたことを示します。 このコードを配置すると、レコードの更新時にコンカレンシー違反が発生したときに、ユーザーの変更は失われます。それが、同時に行われた別のユーザーによる変更を上書きしてしまうためです。 特に、GridView は編集前の状態に戻され、現在のデータベース データにバインドされます。 これにより、GridView 行は、以前は表示されていなかった他のユーザーの変更で更新されます。 さらに、UpdateConflictMessage
ラベル コントロールには、発生した内容に関する説明がユーザーにわかるように表示されます。 この一連のイベントの詳細については、図 19 を参照してください。
図 19: コンカレンシー違反が発生した場合、ユーザーの更新が失われる (クリックするとフルサイズの画像が表示されます)
Note
また、GridView を編集前の状態に戻すのではなく、渡された GridViewUpdatedEventArgs
オブジェクトの KeepInEditMode
プロパティを true に設定することで、GridView を編集状態のままにすることもできます。 ただし、この方法を使用する場合は、(DataBind()
メソッドを呼び出して) データを GridView に再バインドして、他のユーザーの値が編集インターフェイスに読み込まれるようにしてください。 このチュートリアルでダウンロードできるコードでは、RowUpdated
イベント ハンドラーでこれらの 2 行のコードがコメントアウトされています。これらのコード行をコメント解除するだけで、コンカレンシー違反後も GridView が編集モードのままになります。
削除時のコンカレンシー違反に対応する
DB ダイレクト パターンでは、コンカレンシー違反が発生したときに、例外が発生しません。 代わりに、データベース ステートメントが、どのレコードにも影響を及ぼさなくなります。これは WHERE 句がどのレコードとも一致しないためです。 BLL で作成されたデータ変更メソッドはすべて、正確に 1 つのレコードに影響を与えたかどうかを示すブール値を返すように設計されています。 そのため、レコードの削除時にコンカレンシー違反が発生したかどうかを判断するには、BLL の DeleteProduct
メソッドの戻り値を調べることができます。
BLL メソッドの戻り値は、イベント ハンドラーに渡された ObjectDataSourceStatusEventArgs
オブジェクトの ReturnValue
プロパティを使用して、ObjectDataSource のポストレベル イベント ハンドラーで調べることができます。 DeleteProduct
メソッドからの戻り値を判断することが目的なので、ObjectDataSource の Deleted
イベントのイベント ハンドラーを作成する必要があります。 ReturnValue
プロパティは object
型で、例外が発生し、メソッドが値を返す前に中断された場合は null
にすることができます。 したがって、まず、ReturnValue
プロパティが null
でないこと、またブール値であることを確認する必要があります。 このチェックが成功すると想定して、ReturnValue
が false
の場合の DeleteConflictMessage
ラベル コントロールを示します。 これを行うには、次のコードを使用します。
protected void ProductsOptimisticConcurrencyDataSource_Deleted(
object sender, ObjectDataSourceStatusEventArgs e)
{
if (e.ReturnValue != null && e.ReturnValue is bool)
{
bool deleteReturnValue = (bool)e.ReturnValue;
if (deleteReturnValue == false)
{
// No row was deleted, display the warning message
DeleteConflictMessage.Visible = true;
}
}
}
コンカレンシー違反が発生した場合、ユーザーの削除要求は取り消されます。 GridView が更新され、ユーザーがページを読み込んでから [削除] ボタンをクリックするまでの間に、そのレコードに対して発生した変更が表示されます。 このような違反が発生すると、DeleteConflictMessage
ラベルが表示され、発生した内容に関する説明が示されます (図 20 を参照)。
図 20: コンカレンシー違反が発生するとユーザーの削除が取り消される (クリックするとフルサイズの画像が表示されます)
まとめ
コンカレンシー違反の機会は、複数のユーザーが同時にデータを更新または削除できるすべてのアプリケーションに存在します。 このような違反が考慮されない場合、2 人のユーザーが同じデータを同時に更新すると、最後に書き込んだユーザーが "優先" され、他のユーザーの変更は上書きされます。 また、開発者がオプティミスティックまたはペシミスティック同時実行制御を実装することもできます。 オプティミスティック同時実行制御では、コンカレンシー違反が頻繁に発生しないことを想定して、コンカレンシー違反を構成する更新または削除コマンドを単純に禁止します。 ペシミスティック同時実行制御では、コンカレンシー違反が頻繁に発生することが想定されるため、1 人のユーザーの更新または削除コマンドを単純に拒否するだけにはいきません。 ペシミスティック同時実行制御では、レコードを更新するときに、そのレコードはロックされます。ロックされている間、そのレコードを他のユーザーが変更したり削除したりすることはできません。
.NET の型指定された DataSet には、オプティミスティック同時実行制御をサポートするための機能が用意されています。 特に、データベースに対して発行される UPDATE
および DELETE
ステートメントには、テーブルのすべての列が含まれます。このため、更新または削除が発生するのは、レコードの現在のデータが、更新または削除の実行時にユーザーが持っていた元のデータと一致する場合だけになります。 DAL がオプティミスティック同時実行制御をサポートするように構成されたら、BLL メソッドを更新する必要があります。 さらに、BLL を呼び出す ASP.NET ページは、ObjectDataSource が、元の値をそのデータ Web コントロールから取得し、BLL に渡すように構成する必要があります。
このチュートリアルで説明したように、ASP.NET Web アプリケーションでオプティミスティック同時実行制御を実装するには、DAL と BLL を更新し、ASP.NET ページでサポートを追加する必要があります。 この追加作業が時間と労力の賢明な投資となるかどうかは、用途によって異なります。 ユーザーが同時にデータを更新する頻度が低い場合、または更新するデータがお互いに異なる場合、同時実行制御は重要な問題ではありません。 しかし、サイト上で複数のユーザーが定期的に同じデータに対して作業を行っている場合は、同時実行制御を使用することで、あるユーザーの更新や削除によって、別のユーザーの更新や削除が無意識のうちに上書きされるのを防ぐことができます。
プログラミングに満足!
著者について
7 冊の ASP/ASP.NET 書籍の著者であり、4GuysFromRolla.com の創設者である Scott Mitchell は、1998 年から Microsoft Web テクノロジを扱っています。 Scott は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズは24時間で2.0 ASP.NET 自分自身を教えています。 にアクセスするか、ブログを使用して にアクセスmitchell@4GuysFromRolla.comできます。これは でhttp://ScottOnWriting.NET見つけることができます。