複数のユーザーがデータを編集できる Web アプリケーションの場合、2 人のユーザーが同じデータを同時に編集する可能性があります。 このチュートリアルでは、このリスクを処理するためのオプティミスティック コンカレンシー制御を実装します。
イントロダクション
ユーザーのみがデータを表示できる Web アプリケーション、またはデータを変更できる 1 人のユーザーのみを含む Web アプリケーションの場合、2 人の同時ユーザーが誤って互いの変更を上書きする脅威はありません。 ただし、複数のユーザーがデータを更新または削除できる Web アプリケーションの場合、あるユーザーの変更が別の同時ユーザーと競合する可能性があります。 コンカレンシー ポリシーが設定されていない場合、2 人のユーザーが同時に 1 つのレコードを編集している場合、最後に変更をコミットしたユーザーは、最初のレコードによって行われた変更をオーバーライドします。
たとえば、Jisun と Sam の 2 人のユーザーが、GridView コントロールを使用して製品を更新および削除できるアプリケーションのページにアクセスしていたとします。 どちらも同じタイミングで GridView の [編集] ボタンをクリックします。 製品名を「チャイティー」に変更し、[更新]ボタンをクリックします。 結果はデータベースに送信される UPDATE
ステートメントであり、製品 のすべての 更新可能なフィールドが設定されます (Jisun が更新したフィールドは 1 つだけですが、 ProductName
)。 この時点で、データベースには、「Chai Tea」という値、飲料のカテゴリ、エキゾチックリキッドというサプライヤーなどが、この特定の製品に含まれます。 ただし、Sam の画面の GridView には、編集可能な GridView 行に "Chai" という製品名が表示されます。 Jisun の変更がコミットされてから数秒後、Sam はカテゴリを Condiments に更新し、[更新] をクリックします。 これにより、 UPDATE
ステートメントがデータベースに送信され、製品名が "Chai" に設定され、 CategoryID
が対応する Beverages カテゴリ ID に設定されます。 製品名に対する Jisun の変更が上書きされました。 図 1 は、この一連のイベントをグラフィカルに示しています。
図 1: 2 人のユーザーがレコードを同時に更新する場合、1 人のユーザーの変更によって他のユーザーの変更が上書きされる可能性がある (フルサイズの画像を表示する 場合はクリックします)
2 人のユーザーが同じページにアクセスしているとき、1 人のユーザーがレコードを更新している最中に、別のユーザーによってそのレコードが削除されてしまう可能性があります。 または、ユーザーがページを読み込んでから [削除] ボタンをクリックしてから、別のユーザーがそのレコードの内容を変更した可能性があります。
次の 3 つの コンカレンシー制御 戦略を使用できます。
- Do Nothing -if 同時実行ユーザーが同じレコードを変更し、最後のコミットが優先されるようにします (既定の動作)
- オプティミスティック コンカレンシー - コンカレンシーの競合が随時発生する可能性がある一方で、そのような競合が発生しない時間の大部分を想定します。したがって、競合が発生した場合は、別のユーザーが同じデータを変更したため、変更を保存できないことをユーザーに通知するだけです
- ペシミスティック コンカレンシー - コンカレンシーの競合が一般的であり、別のユーザーの同時アクティビティのために変更が保存されなかったとユーザーが伝えられるのを許容しないことを前提としています。そのため、1 人のユーザーがレコードの更新を開始するとロックされるため、ユーザーが変更をコミットするまで、他のユーザーがそのレコードを編集または削除できなくなります。
ここまでのすべてのチュートリアルでは、既定のコンカレンシー解決戦略を使用してきました。つまり、「最後の書き込みが有効になる」方式です。 このチュートリアルでは、オプティミスティック コンカレンシー制御を実装する方法について説明します。
注
このチュートリアル シリーズでは、ペシミスティック コンカレンシーの例については説明しません。 このようなロックが適切に放棄されていない場合、他のユーザーがデータを更新できなくなる可能性があるため、ペシミスティック コンカレンシーはほとんど使用されません。 たとえば、ユーザーが編集のためにレコードをロックし、ロックを解除する前にその日を終了した場合、元のユーザーが戻って更新を完了するまで、他のユーザーはそのレコードを更新できません。 したがって、ペシミスティック コンカレンシーが使用される状況では、通常、タイムアウトが発生し、到達するとロックが取り消されます。 チケット販売 Web サイトは、ユーザーが注文プロセスを完了している間に特定の座席の場所を短時間ロックする、ペシミスティック コンカレンシー制御の例です。
手順 1: オプティミスティック コンカレンシーの実装方法を確認する
オプティミスティック コンカレンシー制御は、更新または削除されるレコードの値が、更新または削除プロセスの開始時と同じであることを確認することによって機能します。 たとえば、編集可能な GridView で [編集] ボタンをクリックすると、レコードの値がデータベースから読み取られ、TextBoxes やその他の Web コントロールに表示されます。 これらの元の値は GridView によって保存されます。 その後、ユーザーが変更を加えて [更新] ボタンをクリックすると、元の値と新しい値がビジネス ロジック レイヤーに送信され、次にデータ アクセス層に送信されます。 データ アクセス層は、ユーザーが編集を開始した元の値がデータベース内の値と同じ場合にのみレコードを更新する SQL ステートメントを発行する必要があります。 図 2 は、この一連のイベントを示しています。
図 2: 更新または削除が成功するには、元の値が現在のデータベース値と等しい必要があります (フルサイズの画像を表示する をクリックします)。
オプティミスティック コンカレンシーを実装するには、さまざまな方法があります (さまざまなオプションを簡単に見るための Peter A. Bromberg の オプティミスティック コンカレンシー更新ロジック を参照してください)。 ADO.NET 型付けされた DataSet には、チェック ボックスを有効にするだけで構成できる実装があります。 Typed 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
注
この UPDATE
ステートメントは、読みやすくするために簡略化されています。 実際には、UnitPrice
句のWHERE
チェックは、UnitPrice
にNULL
を含め、NULL = NULL
が常に False を返すかどうかを確認できるため、より複雑になります (代わりにIS NULL
を使用する必要があります)。
基になる別の UPDATE
ステートメントを使用するだけでなく、オプティミスティック コンカレンシーを使用するように TableAdapter を構成すると、その DB ダイレクト メソッドのシグネチャも変更されます。 最初のチュートリアル「 データ アクセス層の作成」では、DB ダイレクト メソッドは、(厳密に型指定された DataRow または DataTable インスタンスではなく) スカラー値のリストを入力パラメーターとして受け入れるメソッドであることを思い出してください。 オプティミスティック コンカレンシーを使用する場合、DB ダイレクト Update()
メソッドと Delete()
メソッドには、元の値の入力パラメーターも含まれます。 さらに、バッチ更新パターン (スカラー値ではなく DataRows と DataTable を受け入れる Update()
メソッド オーバーロード) を使用するための BLL 内のコードも変更する必要があります。
オプティミスティック コンカレンシーを使用するように既存の DAL の TableAdapter を拡張するのではなく (対応するために BLL を変更する必要があります)、代わりに NorthwindOptimisticConcurrency
という名前の新しい型指定されたデータセットを作成しましょう。このデータセットには、オプティミスティック コンカレンシーを使用する Products
TableAdapter を追加します。 その後、オプティミスティック コンカレンシー DAL をサポートするために適切な変更を加える ProductsOptimisticConcurrencyBLL
Business Logic Layer クラスを作成します。 この基礎が整ったら、ASP.NET ページを作成する準備が整います。
手順 2: オプティミスティック コンカレンシーをサポートするデータ アクセス層を作成する
新しい型指定された DataSet を作成するには、DAL
フォルダー内のApp_Code
フォルダーを右クリックし、NorthwindOptimisticConcurrency
という名前の新しい DataSet を追加します。 最初のチュートリアルで説明したように、これにより、新しい TableAdapter が型指定された DataSet に追加され、TableAdapter 構成ウィザードが自動的に起動されます。 最初の画面では、接続するデータベースを指定するように求められます。NORTHWNDConnectionString
のWeb.config
設定を使用して、同じ 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 ダイレクト メソッドを作成する必要があることを示します。 元の DAL で使用した名前付け規則を反映するように、Return a DataTable パターンのメソッド名を GetData から GetProducts に変更します。
図 7: TableAdapter ですべてのデータ アクセス パターンを利用させる (フルサイズの画像を表示する をクリックします)
ウィザードが完了すると、DataSet デザイナーには、厳密に型指定された DataTable Products
と TableAdapter が含まれます。 DataTable の名前を Products
から ProductsOptimisticConcurrency
に変更します。これを行うには、DataTable のタイトル バーを右クリックし、コンテキスト メニューから [名前の変更] を選択します。
図 8: DataTable と TableAdapter が型指定されたデータセットに追加されました (フルサイズの画像を表示する をクリックします)。
UPDATE
TableAdapter (オプティミスティック コンカレンシーを使用) と Products TableAdapter (オプティミスティック コンカレンシーを使用しない) の間のDELETE
クエリとProductsOptimisticConcurrency
クエリの違いを確認するには、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 の WHERE
ステートメントのDELETE
句には、Product
テーブルの既存の各列値と、GridView (または DetailsView または FormView) が最後に設定された時点の元の値との比較が含まれています。
ProductID
、ProductName
、およびDiscontinued
以外のすべてのフィールドにはNULL
値が含まれる可能性があるため、NULL
句のWHERE
値を正しく比較するために、追加のパラメーターとチェックが含まれます。
このチュートリアルでは、オプティミスティック コンカレンシー対応 DataSet に DataTable を追加しません。ASP.NET ページでは製品情報の更新と削除のみが提供されるためです。 ただし、GetProductByProductID(productID)
TableAdapter に ProductsOptimisticConcurrency
メソッドを追加する必要があります。
これを行うには、TableAdapter のタイトル バー ( Fill
と GetProducts
メソッド名のすぐ上の領域) を右クリックし、コンテキスト メニューから [クエリの追加] を選択します。 これにより、TableAdapter クエリ構成ウィザードが起動します。 TableAdapter の初期構成と同様に、アドホック SQL ステートメントを使用して GetProductByProductID(productID)
メソッドを作成することを選択します (図 4 を参照)。
GetProductByProductID(productID)
メソッドは特定の製品に関する情報を返すので、このクエリが行を返すSELECT
クエリ型であることを示します。
図 9: "行を返すSELECT
" としてクエリの種類をマークします (フルサイズの画像を表示する をクリックします)。
次の画面では、TableAdapter の既定のクエリが事前に読み込まれた状態で、SQL クエリを使用するように求められます。 図 10 に示すように、 WHERE ProductID = @ProductID
句を含むように既存のクエリを拡張します。
図 10: 事前に読み込まれたクエリに WHERE
句を追加して特定の製品レコードを返す (フルサイズの画像を表示する をクリックします)
最後に、生成されたメソッド名を FillByProductID
および GetProductByProductID
に変更します。
図 11: FillByProductID
と GetProductByProductID
にメソッドの名前を変更します (フルサイズの画像を表示する をクリックします)。
このウィザードが完了すると、TableAdapter には、GetProducts()
製品を返す と、指定した製品を返す GetProductByProductID(productID)
の 2 つのメソッドが含まれるようになりました。
手順 3: オプティミスティック Concurrency-Enabled DAL のビジネス ロジック レイヤーを作成する
既存の ProductsBLL
クラスには、バッチ更新パターンと DB ダイレクト パターンの両方を使用する例があります。
AddProduct
メソッドと UpdateProduct
オーバーロードはどちらもバッチ更新パターンを使用し、ProductRow
インスタンスを TableAdapter の Update メソッドに渡します。 一方、 DeleteProduct
メソッドは DB ダイレクト パターンを使用し、TableAdapter の Delete(productID)
メソッドを呼び出します。
新しい ProductsOptimisticConcurrency
TableAdapter では、DB ダイレクト メソッドで元の値も渡す必要があります。 たとえば、 Delete
メソッドでは、元の ProductID
、 ProductName
、 SupplierID
、 CategoryID
、 QuantityPerUnit
、 UnitPrice
、 UnitsInStock
、 UnitsOnOrder
、 ReorderLevel
、 Discontinued
の 10 個の入力パラメーターが必要になりました。 データベースに送信されたWHERE
ステートメントのDELETE
句でこれらの追加の入力パラメーターの値が使用され、データベースの現在の値が元の値にマップされている場合にのみ、指定されたレコードが削除されます。
バッチ更新パターンで使用される TableAdapter の Update
メソッドのメソッド シグネチャは変更されていませんが、元の値と新しい値を記録するために必要なコードには含まれています。 そのため、オプティミスティック コンカレンシー対応 DAL を既存の ProductsBLL
クラスで使用するのではなく、新しい DAL を操作するための新しいビジネス ロジック レイヤー クラスを作成しましょう。
ProductsOptimisticConcurrencyBLL
フォルダー内の BLL
フォルダーに App_Code
という名前のクラスを追加します。
図 12: ProductsOptimisticConcurrencyBLL
クラスを BLL フォルダーに追加する
次に、 ProductsOptimisticConcurrencyBLL
クラスに次のコードを追加します。
Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
Get
If _productsAdapter Is Nothing Then
_productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
End If
Return _productsAdapter
End Get
End Property
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Select, True)> _
Public Function GetProducts() As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
Return Adapter.GetProducts()
End Function
End Class
クラス宣言の先頭の using NorthwindOptimisticConcurrencyTableAdapters
ステートメントに注意してください。
NorthwindOptimisticConcurrencyTableAdapters
名前空間には、DAL のメソッドを提供するProductsOptimisticConcurrencyTableAdapter
クラスが含まれています。 また、クラス宣言の前に、 System.ComponentModel.DataObject
属性があります。これにより、このクラスを ObjectDataSource ウィザードのドロップダウン リストに含むように Visual Studio に指示されます。
ProductsOptimisticConcurrencyBLL
のAdapter
プロパティは、ProductsOptimisticConcurrencyTableAdapter
クラスのインスタンスにすばやくアクセスでき、元の BLL クラス (ProductsBLL
、CategoriesBLL
など) で使用されるパターンに従います。 最後に、GetProducts()
メソッドは DAL のGetProducts()
メソッドを呼び出し、データベース内の各製品レコードのProductsOptimisticConcurrencyDataTable
インスタンスが設定されたProductsOptimisticConcurrencyRow
オブジェクトを返します。
オプティミスティック コンカレンシーを使用した DB ダイレクト パターンを使用した製品の削除
オプティミスティック コンカレンシーを使用する DAL に対して DB ダイレクト パターンを使用する場合は、メソッドに新しい値と元の値を渡す必要があります。 削除する場合、新しい値は存在しないため、元の値のみを渡す必要があります。 BLL では、元のすべてのパラメーターを入力パラメーターとして受け入れる必要があります。
DeleteProduct
クラスの ProductsOptimisticConcurrencyBLL
メソッドで DB ダイレクト メソッドを使用してみましょう。 つまり、次のコードに示すように、このメソッドは 10 個の製品データ フィールドをすべて入力パラメーターとして取り込み、DAL に渡す必要があります。
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
ByVal original_productID As Integer, ByVal original_productName As String, _
ByVal original_supplierID As Nullable(Of Integer), _
ByVal original_categoryID As Nullable(Of Integer), _
ByVal original_quantityPerUnit As String, _
ByVal original_unitPrice As Nullable(Of Decimal), _
ByVal original_unitsInStock As Nullable(Of Short), _
ByVal original_unitsOnOrder As Nullable(Of Short), _
ByVal original_reorderLevel As Nullable(Of Short), _
ByVal original_discontinued As Boolean) _
As Boolean
Dim rowsAffected As Integer = 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
End Function
元の値 (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 の
ProductRow
メソッドを使用して、現在のデータベース製品情報をGetProductByProductID(productID)
インスタンスに読み取ります - 手順 1 の
ProductRow
インスタンスに新しい値を割り当てる - TableAdapter の
Update
メソッドを呼び出し、ProductRow
インスタンスを渡します
ただし、この一連の手順では、オプティミスティック コンカレンシーが正しくサポートされません。これは、手順 1 で設定された ProductRow
がデータベースから直接入力されるためです。つまり、DataRow によって使用される元の値は、編集プロセスの開始時に GridView にバインドされた値ではなく、データベースに現在存在する値です。 代わりに、オプティミスティック コンカレンシーが有効な DAL を使用する場合は、次の手順を使用するように UpdateProduct
メソッドのオーバーロードを変更する必要があります。
- TableAdapter の
ProductsOptimisticConcurrencyRow
メソッドを使用して、現在のデータベース製品情報をGetProductByProductID(productID)
インスタンスに読み取ります - 手順 1 の インスタンスに
ProductsOptimisticConcurrencyRow
の値を割り当てる -
ProductsOptimisticConcurrencyRow
インスタンスのAcceptChanges()
メソッドを呼び出します。これにより、現在の値が "元の" 値であることを DataRow に指示します -
インスタンスに
ProductsOptimisticConcurrencyRow
値を割り当てる - TableAdapter の
Update
メソッドを呼び出し、ProductsOptimisticConcurrencyRow
インスタンスを渡します
手順 1 では、指定した製品レコードのすべての現在のデータベース値を読み取ります。 この手順は、UpdateProduct
製品列を更新するオーバーロードでは余分ですが (これらの値は手順 2 で上書きされるため)、列値のサブセットのみが入力パラメーターとして渡されるオーバーロードには不可欠です。 元の値が ProductsOptimisticConcurrencyRow
インスタンスに割り当てられると、AcceptChanges()
メソッドが呼び出され、現在の DataRow 値が、@original_ColumnName
ステートメントのUPDATE
パラメーターで使用される元の値としてマークされます。 次に、新しいパラメーター値が ProductsOptimisticConcurrencyRow
に割り当てられ、最後に Update
メソッドが呼び出され、DataRow が渡されます。
次のコードは、すべての製品データ フィールドを入力パラメーターとして受け入れる UpdateProduct
オーバーロードを示しています。 ここでは示されていませんが、このチュートリアルのダウンロードに含まれる ProductsOptimisticConcurrencyBLL
クラスには、製品の名前と価格のみを入力パラメーターとして受け入れる UpdateProduct
オーバーロードも含まれています。
Protected Sub AssignAllProductValues( _
ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
ByVal discontinued As Boolean)
product.ProductName = productName
If Not supplierID.HasValue Then
product.SetSupplierIDNull()
Else
product.SupplierID = supplierID.Value
End If
If Not categoryID.HasValue Then
product.SetCategoryIDNull()
Else
product.CategoryID = categoryID.Value
End If
If quantityPerUnit Is Nothing Then
product.SetQuantityPerUnitNull()
Else
product.QuantityPerUnit = quantityPerUnit
End If
If Not unitPrice.HasValue Then
product.SetUnitPriceNull()
Else
product.UnitPrice = unitPrice.Value
End If
If Not unitsInStock.HasValue Then
product.SetUnitsInStockNull()
Else
product.UnitsInStock = unitsInStock.Value
End If
If Not unitsOnOrder.HasValue Then
product.SetUnitsOnOrderNull()
Else
product.UnitsOnOrder = unitsOnOrder.Value
End If
If Not reorderLevel.HasValue Then
product.SetReorderLevelNull()
Else
product.ReorderLevel = reorderLevel.Value
End If
product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
ByVal discontinued As Boolean, ByVal productID As Integer, _
_
ByVal original_productName As String, _
ByVal original_supplierID As Nullable(Of Integer), _
ByVal original_categoryID As Nullable(Of Integer), _
ByVal original_quantityPerUnit As String, _
ByVal original_unitPrice As Nullable(Of Decimal), _
ByVal original_unitsInStock As Nullable(Of Short), _
ByVal original_unitsOnOrder As Nullable(Of Short), _
ByVal original_reorderLevel As Nullable(Of Short), _
ByVal original_discontinued As Boolean, _
ByVal original_productID As Integer) _
As Boolean
'STEP 1: Read in the current database product information
Dim products As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
Adapter.GetProductByProductID(original_productID)
If products.Count = 0 Then
' no matching record found, return false
Return False
End If
Dim product As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = 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
Dim rowsAffected As Integer = Adapter.Update(product)
' Return true if precisely one row was updated, otherwise false
Return rowsAffected = 1
End Function
手順 4: ASP.NET ページから BLL メソッドに元の値と新しい値を渡す
DAL と BLL が完了したら、残っているのは、システムに組み込まれているオプティミスティック コンカレンシー ロジックを利用できる ASP.NET ページを作成することです。 具体的には、データ Web コントロール (GridView、DetailsView、または FormView) は元の値を記憶する必要があり、ObjectDataSource は両方の値のセットをビジネス ロジック レイヤーに渡す必要があります。 さらに、コンカレンシー違反を適切に処理するように ASP.NET ページを構成する必要があります。
まず、OptimisticConcurrency.aspx
フォルダー内のEditInsertDelete
ページを開き、デザイナーに GridView を追加し、そのID
プロパティを ProductsGrid
に設定します。 GridView のスマート タグから、 ProductsOptimisticConcurrencyDataSource
という名前の新しい ObjectDataSource を作成することを選択します。 この ObjectDataSource でオプティミスティック コンカレンシーをサポートする DAL を使用する必要があるため、 ProductsOptimisticConcurrencyBLL
オブジェクトを使用するように構成します。
図 13: ObjectDataSource に ProductsOptimisticConcurrencyBLL
オブジェクトを使用させる (フルサイズの画像を表示する をクリックします)
ウィザードのドロップダウン リストから、 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
コレクションには、Parameter
クラスの ProductsOptimisticConcurrencyBLL
メソッド内の 10 個の入力パラメーターごとにDeleteProduct
インスタンスが含まれています。 同様に、UpdateParameters
コレクションには、Parameter
の各入力パラメーターのUpdateProduct
インスタンスが含まれます。
データの変更に関連する以前のチュートリアルでは、この時点で ObjectDataSource の OldValuesParameterFormatString
プロパティを削除します。このプロパティは、BLL メソッドが古い (または元の) 値と新しい値が渡されることを示しているためです。 さらに、このプロパティ値は、元の値の入力パラメーター名を示します。 元の値を BLL に渡しているため、このプロパティを削除 しないでください 。
注
OldValuesParameterFormatString
プロパティの値は、元の値を予期する BLL の入力パラメーター名にマップする必要があります。 これらのパラメーターに original_productName
、 original_supplierID
などの名前を付けたので、 OldValuesParameterFormatString
プロパティの値は original_{0}
のままにしておくことができます。 ただし、BLL メソッドの入力パラメーターに old_productName
、 old_supplierID
などの名前がある場合は、 OldValuesParameterFormatString
プロパティを old_{0}
に更新する必要があります。
ObjectDataSource が元の値を BLL メソッドに正しく渡すには、最終的なプロパティ設定を 1 つ行う必要があります。 ObjectDataSource には、次の 2 つの値のいずれかに割り当てることができる ConflictDetection プロパティがあります。
-
OverwriteChanges
- 既定値。は、元の値を BLL メソッドの元の入力パラメーターに送信しません -
CompareAllValues
- 元の値を BLL メソッドに送信します。オプティミスティック コンカレンシーを使用する場合は、このオプションを選択します
ConflictDetection
プロパティを CompareAllValues
に設定します。
GridView のプロパティとフィールドの構成
ObjectDataSource のプロパティが正しく構成された状態で、GridView の設定に注目しましょう。 まず、GridView で編集と削除をサポートする必要があるため、GridView のスマート タグから [編集を有効にする] チェック ボックスと [削除を有効にする] チェック ボックスをクリックします。 これにより、 ShowEditButton
と ShowDeleteButton
の両方が true
に設定されている CommandField が追加されます。
ProductsOptimisticConcurrencyDataSource
ObjectDataSource にバインドされたとき、GridView には各プロダクトのデータフィールドが含まれます。 このような GridView は編集可能ですが、ユーザー エクスペリエンスは到底許容できるものではありません。
CategoryID
および SupplierID
BoundFields は TextBox としてレンダリングされ、ユーザーは適切なカテゴリとサプライヤーを ID 番号として入力する必要があります。 数値フィールドの書式設定が行われず、製品名が提供されていることや、単価、在庫数、注文数、再発注水準の値が適切な数値であり、0以上であることを確認する検証コントロールもありません。
「編集および挿入インターフェイスへの検証コントロールの追加」および「データ変更インターフェイスのカスタマイズ」チュートリアルで説明したように、BoundFields を TemplateFields に置き換えることで、ユーザー インターフェイスをカスタマイズできます。 私はこの GridView とその編集インターフェイスを次の方法で変更しました。
- BoundFields の
ProductID
、SupplierName
、CategoryName
を削除しました -
ProductName
BoundField を TemplateField に変換し、RequiredFieldValidation コントロールを追加しました。 -
CategoryID
とSupplierID
BoundFields を TemplateFields に変換し、TextBoxes ではなく DropDownLists を使用するように編集インターフェイスを調整しました。 これらの TemplateFields のItemTemplates
では、CategoryName
とSupplierName
のデータ フィールドが表示されます。 - BoundFields の
UnitPrice
、UnitsInStock
、UnitsOnOrder
、ReorderLevel
を 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>
もうすぐ完全に機能する例が出来上がります。 しかし、いくつかの微妙な点が忍び寄って問題を引き起こします。 さらに、コンカレンシー違反が発生したときにユーザーに警告するインターフェイスも必要です。
注
データ 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
内の製品の結果を削除しようとしています (フルサイズの画像を表示する をクリックします)。
FormatException
は、ObjectDataSource が元のUnitPrice
値を読み取ろうとしたときに発生します。
ItemTemplate
には通貨 (UnitPrice
) として書式設定された<%# Bind("UnitPrice", "{0:C}") %>
があるため、19.95 ドルなどの通貨記号が含まれます。
FormatException
は、ObjectDataSource がこの文字列をdecimal
に変換しようとしたときに発生します。 この問題を回避するために、いくつかのオプションがあります。
-
ItemTemplate
から通貨書式を削除します。 つまり、<%# Bind("UnitPrice", "{0:C}") %>
を使用する代わりに、<%# Bind("UnitPrice") %>
を使用するだけです。 この欠点は、価格がフォーマットされなくなったということです。 -
UnitPrice
に通貨として書式設定されたItemTemplate
を表示しますが、これを行うにはEval
キーワードを使用します。Eval
は一方向のデータ バインドを実行することを思い出してください。 元の値にUnitPrice
値を指定する必要があるため、ItemTemplate
には双方向のデータ バインド ステートメントが必要ですが、これは、Visible
プロパティがfalse
に設定されている Label 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>
-
ItemTemplate
を使用して、<%# Bind("UnitPrice") %>
から通貨書式を削除します。 GridView のRowDataBound
イベント ハンドラーで、UnitPrice
値が表示される Label Web コントロールにプログラムでアクセスし、そのText
プロパティを書式設定されたバージョンに設定します。 -
UnitPrice
は通貨として書式設定したままにしておきます。 GridView のRowDeleting
イベント ハンドラーで、既存の元のUnitPrice
値 ($19.95) を、Decimal.Parse
を使用して実際の 10 進値に置き換えます。 ASP.NET ページチュートリアルの BLL 例外と DAL-Level 例外の処理のRowUpdating
イベント ハンドラーで、同様のことを行う方法について説明しました。
私の例では、書式設定されていないText
値にバインドされた双方向データであるUnitPrice
プロパティを持つ非表示の Label Web コントロールを追加して、2 番目のアプローチを選択しました。
この問題を解決したら、製品の [削除] ボタンをもう一度クリックしてみてください。 今回は、ObjectDataSource が BLL の InvalidOperationException
メソッドを呼び出そうとしたときにUpdateProduct
を取得します。
図 16: ObjectDataSource は、送信する入力パラメーターを持つメソッドを見つけることができません (フルサイズの画像を表示する をクリックします)。
例外のメッセージを見ると、ObjectDataSource が、DeleteProduct
とoriginal_CategoryName
入力パラメーターを含む BLL original_SupplierName
メソッドを呼び出したいと考えているのは明らかです。 これは、ItemTemplate
およびCategoryID
TemplateFields のSupplierID
には、現在、CategoryName
およびSupplierName
データ フィールドを含む双方向の Bind ステートメントが含まれているためです。 代わりに、Bind
とCategoryID
のデータ フィールドにSupplierID
ステートメントを含める必要があります。 これを実現するには、既存の Bind ステートメントを Eval
ステートメントに置き換え、Text
および CategoryID
データフィールドに双方向データバインディングされる非表示の Label コントロールを追加します。手順は、下記に示すように行います。
<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
句が満たすレコードがデータベースにないため、0 行に影響します。
DeleteProduct
メソッドはfalse
を返し、ObjectDataSource のデータは GridView にリバインドされます。
エンド ユーザーの観点から見ると、2 番目のブラウザー ウィンドウで Chai Tea の [削除] ボタンをクリックすると画面が点滅し、戻ってくると製品はまだそこにありますが、"Chai" (最初のブラウザー インスタンスによって行われた製品名の変更) として表示されます。 ユーザーが [削除] ボタンをもう一度クリックすると、GridView の元の ProductName
値 ("Chai") がデータベース内の値と一致するようになったので、削除は成功します。
どちらの場合も、ユーザー エクスペリエンスは理想的ではありません。 バッチ更新パターンを使用する場合、 DBConcurrencyException
例外の詳細をユーザーに表示したくないことは明らかです。 また、DB ダイレクト パターンを使用する場合の動作は、users コマンドが失敗するためやや混乱しますが、その理由は正確に示されていませんでした。
これら 2 つの問題を解決するために、更新または削除が失敗した理由の説明を提供するラベル Web コントロールをページに作成できます。 バッチ更新パターンの場合、GridView のポストレベル イベント ハンドラーで DBConcurrencyException
例外が発生したかどうかを判断し、必要に応じて警告ラベルを表示できます。 DB ダイレクト メソッドの場合は、BLL メソッドの戻り値 (1 行が影響を受けた場合は true
、それ以外の場合は false
) を調べ、必要に応じて情報メッセージを表示できます。
手順 6: 情報メッセージを追加し、コンカレンシー違反が発生した場合に表示する
コンカレンシー違反が発生した場合の動作は、DAL のバッチ更新と DB 直接パターンのどちらを使用したかによって異なります。 このチュートリアルでは、両方のパターンを使用します。バッチ更新パターンは更新に使用され、DB ダイレクト パターンは削除に使用されます。 まず、データを削除または更新しようとしたときにコンカレンシー違反が発生したことを説明する 2 つの Label Web コントロールをページに追加しましょう。 Label コントロールの 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 つのラベル コントロールがページに追加されました (フルサイズの画像を表示する をクリックします)。
これらの Label Web コントロールが配置されたので、コンカレンシー違反がいつ発生したかを判断する方法を調べる準備ができました。その時点で、適切な Label の Visible
プロパティを true
に設定して、情報メッセージを表示できます。
更新時のコンカレンシー違反の処理
最初に、バッチ更新パターンを使用するときにコンカレンシー違反を処理する方法を見てみましょう。 バッチ更新パターンでこのような違反が発生すると、 DBConcurrencyException
例外がスローされるため、ASP.NET ページにコードを追加して、更新プロセス中に DBConcurrencyException
例外が発生したかどうかを判断する必要があります。 その場合は、別のユーザーがレコードの編集を開始してから [更新] ボタンをクリックしてから同じデータを変更したため、変更が保存されなかったことを説明するメッセージをユーザーに表示する必要があります。
ASP.NET ページのチュートリアルの BLL 例外と DAL-Level 例外の処理に 関するチュートリアルで説明したように、このような例外は、データ Web コントロールのポストレベル イベント ハンドラーで検出および抑制できます。 したがって、GridView のRowUpdated
イベントに対して、DBConcurrencyException
例外がスローされたかどうかを確認するイベント ハンドラーを作成する必要があります。 このイベント ハンドラーは、次のイベント ハンドラー コードに示すように、更新プロセス中に発生したすべての例外への参照を渡します。
Protected Sub ProductsGrid_RowUpdated _
(ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
Handles ProductsGrid.RowUpdated
If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
' Display the warning message and note that the exception has
' been handled...
UpdateConflictMessage.Visible = True
e.ExceptionHandled = True
End If
End If
End Sub
DBConcurrencyException
例外が発生した場合、このイベント ハンドラーは UpdateConflictMessage
Label コントロールを表示し、例外が処理されたことを示します。 このコードを配置すると、レコードの更新時にコンカレンシー違反が発生すると、ユーザーの変更は失われます。これは、別のユーザーの変更が同時に上書きされるためです。 特に、GridView は編集前の状態に戻され、現在のデータベース データにバインドされます。 これにより、GridView 行が、以前は表示されなかった他のユーザーの変更で更新されます。 さらに、 UpdateConflictMessage
ラベル コントロールは、発生した内容をユーザーに説明します。 この一連のイベントの詳細については、図 19 を参照してください。
図 19: コンカレンシー違反が発生した場合にユーザーの更新が失われる (フルサイズの画像を表示する をクリックします)
注
または、GridView を編集前の状態に戻す代わりに、渡されたKeepInEditMode
オブジェクトのGridViewUpdatedEventArgs
プロパティを true に設定することで、GridView を編集状態のままにすることもできます。 ただし、この方法を使用する場合は、他のユーザーの値が編集インターフェイスに読み込まれるように、( DataBind()
メソッドを呼び出して) データを GridView に再バインドしてください。 このチュートリアルでダウンロードできるコードには、 RowUpdated
イベント ハンドラー内のこれら 2 行のコードがコメント アウトされています。これらのコード行のコメントを解除するだけで、コンカレンシー違反の後も GridView が編集モードのままになります。
削除時のコンカレンシー違反への対応
DB ダイレクト パターンでは、コンカレンシー違反が発生しても例外は発生しません。 代わりに、WHERE 句はどのレコードとも一致しないため、データベース ステートメントは単にレコードに影響しません。 BLL で作成されたすべてのデータ変更メソッドは、正確に 1 つのレコードに影響を与えたかどうかを示すブール値を返すように設計されています。 そのため、レコードの削除時にコンカレンシー違反が発生したかどうかを判断するために、BLL の DeleteProduct
メソッドの戻り値を調べることができます。
BLL メソッドの戻り値は、イベント ハンドラーに渡されたReturnValue
オブジェクトのObjectDataSourceStatusEventArgs
プロパティを使用して、ObjectDataSource のポストレベル イベント ハンドラーで調べることができます。
DeleteProduct
メソッドからの戻り値を決定することに関心があるため、ObjectDataSource のDeleted
イベントのイベント ハンドラーを作成する必要があります。
ReturnValue
プロパティはobject
型であり、例外が発生した場合や、メソッドが値を返す前に割り込まれた場合にはnull
に設定される可能性があります。 したがって、まず、 ReturnValue
プロパティが null
ではなく、ブール値であることを確認する必要があります。 このチェックに合格した場合、DeleteConflictMessage
がReturnValue
されている場合は、false
Label コントロールが表示されます。 これは、次のコードを使用して実現できます。
Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
(ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
Handles ProductsOptimisticConcurrencyDataSource.Deleted
If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
If deleteReturnValue = False Then
' No row was deleted, display the warning message
DeleteConflictMessage.Visible = True
End If
End If
End Sub
コンカレンシー違反が発生した場合、ユーザーの削除要求は取り消されます。 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 は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズ・ティーチ・セルフ ASP.NET 24時間で2.0です。 彼には mitchell@4GuysFromRolla.comで連絡できます。