次の方法で共有


オプティミスティック コンカレンシーを実装する (C#)

スコット・ミッチェル著

PDF をダウンロードする

複数のユーザーがデータを編集できる 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 は、この一連のイベントをグラフィカルに示しています。

2 人のユーザーが同時にレコードを更新する場合、1 人のユーザーの変更が他のユーザーの変更を上書きする可能性があります。

図 1: 2 人のユーザーがレコードを同時に更新する場合、一方のユーザーの変更が他方のユーザーの変更を上書きする可能性がある (フルサイズの画像を表示するにはクリックしてください)

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チェックは、UnitPriceNULLを含め、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 構成ウィザードが自動的に起動されます。 最初の画面では、接続するデータベースを指定するように求められます。NORTHWNDConnectionStringWeb.config設定を使用して、同じ Northwind データベースに接続します。

同じ Northwind データベースに接続する

図 3: 同じ Northwind データベースに接続する (フルサイズの画像を表示する をクリックします)

次に、アドホック SQL ステートメント、新しいストアド プロシージャ、または既存のストアド プロシージャを使用して、データのクエリを実行する方法を求められます。 元の DAL でアドホック SQL クエリを使用したので、ここでもこのオプションを使用します。

アドホック 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

元の DAL の Products TableAdapter から同じ SQL クエリを使用する

図 5: 元の DAL の Products TableAdapter から同じ SQL クエリを使用する (フルサイズの画像を表示する をクリックします)。

次の画面に進む前に、[詳細オプション] ボタンをクリックします。 この TableAdapter でオプティミスティック コンカレンシー制御を使用するには、[オプティミスティック コンカレンシーを使用する] チェック ボックスをオンにします。

[オプティミスティック コンカレンシーを使用する] チェック ボックスをオンにしてオプティミスティック コンカレンシー制御を有効にする

図 6: [オプティミスティック コンカレンシーの使用] チェック ボックスをオンにしてオプティミスティック コンカレンシー制御を有効にする (フルサイズの画像を表示する をクリックします)

最後に、TableAdapter が DataTable を満たし、DataTable を返すデータ アクセス パターンを使用する必要があることを示します。また、DB ダイレクト メソッドを作成する必要があることを示します。 元の DAL で使用した名前付け規則を反映するように、Return a DataTable パターンのメソッド名を GetData から GetProducts に変更します。

TableAdapter ですべてのデータ アクセス パターンを利用する

図 7: TableAdapter ですべてのデータ アクセス パターンを利用させる (フルサイズの画像を表示する をクリックします)

ウィザードが完了すると、DataSet デザイナーには、厳密に型指定された DataTable Products と TableAdapter が含まれます。 DataTable の名前を Products から ProductsOptimisticConcurrency に変更します。これを行うには、DataTable のタイトル バーを右クリックし、コンテキスト メニューから [名前の変更] を選択します。

型指定された DataSet に DataTable と TableAdapter が追加されました

図 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) が最後に設定された時点の元の値との比較が含まれています。 ProductIDProductName、およびDiscontinued以外のすべてのフィールドにはNULL値が含まれる可能性があるため、NULL句のWHERE値を正しく比較するために、追加のパラメーターとチェックが含まれます。

このチュートリアルでは、オプティミスティック コンカレンシー対応 DataSet に DataTable を追加しません。ASP.NET ページでは製品情報の更新と削除のみが提供されるためです。 ただし、GetProductByProductID(productID) TableAdapter に ProductsOptimisticConcurrency メソッドを追加する必要があります。

これを行うには、TableAdapter のタイトル バー ( FillGetProducts メソッド名のすぐ上の領域) を右クリックし、コンテキスト メニューから [クエリの追加] を選択します。 これにより、TableAdapter クエリ構成ウィザードが起動します。 TableAdapter の初期構成と同様に、アドホック SQL ステートメントを使用して GetProductByProductID(productID) メソッドを作成することを選択します (図 4 を参照)。 GetProductByProductID(productID) メソッドは特定の製品に関する情報を返すので、このクエリが行を返すSELECTクエリ型であることを示します。

クエリの種類を

図 9: "行を返すSELECT " としてクエリの種類をマークします (フルサイズの画像を表示する をクリックします)。

次の画面では、TableAdapter の既定のクエリが事前に読み込まれた状態で、SQL クエリを使用するように求められます。 図 10 に示すように、 WHERE ProductID = @ProductID句を含むように既存のクエリを拡張します。

特定の製品レコードを返す事前読み込みクエリに WHERE 句を追加する

図 10: 事前に読み込まれたクエリに WHERE 句を追加して特定の製品レコードを返す (フルサイズの画像を表示する をクリックします)

最後に、生成されたメソッド名を FillByProductID および GetProductByProductIDに変更します。

メソッドの名前を FillByProductID と GetProductByProductID に変更する

図 11: FillByProductIDGetProductByProductID にメソッドの名前を変更します (フルサイズの画像を表示する をクリックします)。

このウィザードが完了すると、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 メソッドでは、元の ProductIDProductNameSupplierIDCategoryIDQuantityPerUnitUnitPriceUnitsInStockUnitsOnOrderReorderLevelDiscontinuedの 10 個の入力パラメーターが必要になりました。 データベースに送信されたWHERE ステートメントのDELETE句でこれらの追加の入力パラメーターの値が使用され、データベースの現在の値が元の値にマップされている場合にのみ、指定されたレコードが削除されます。

バッチ更新パターンで使用される TableAdapter の Update メソッドのメソッド シグネチャは変更されていませんが、元の値と新しい値を記録するために必要なコードには含まれています。 そのため、オプティミスティック コンカレンシー対応 DAL を既存の ProductsBLL クラスで使用するのではなく、新しい DAL を操作するための新しいビジネス ロジック レイヤー クラスを作成しましょう。

ProductsOptimisticConcurrencyBLL フォルダー内の BLL フォルダーに App_Code という名前のクラスを追加します。

ProductsOptimisticConcurrencyBLL クラスを 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 属性があります。これにより、このクラスを ObjectDataSource ウィザードのドロップダウン リストに含むように Visual Studio に指示されます。

ProductsOptimisticConcurrencyBLLAdapter プロパティは、ProductsOptimisticConcurrencyTableAdapter クラスのインスタンスにすばやくアクセスでき、元の BLL クラス (ProductsBLLCategoriesBLL など) で使用されるパターンに従います。 最後に、GetProducts() メソッドは DAL のGetProducts() メソッドを呼び出し、データベース内の各製品レコードのProductsOptimisticConcurrencyDataTable インスタンスが設定されたProductsOptimisticConcurrencyRow オブジェクトを返します。

オプティミスティック コンカレンシーを使用した DB ダイレクト パターンを使用した製品の削除

オプティミスティック コンカレンシーを使用する DAL に対して DB ダイレクト パターンを使用する場合は、メソッドに新しい値と元の値を渡す必要があります。 削除する場合、新しい値は存在しないため、元の値のみを渡す必要があります。 BLL では、元のすべてのパラメーターを入力パラメーターとして受け入れる必要があります。 DeleteProduct クラスの ProductsOptimisticConcurrencyBLL メソッドで 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 を使用) で、バッチ更新パターンを使用して製品情報を更新する場合、コードは次の一連のイベントを実行します。

  1. TableAdapter の ProductRow メソッドを使用して、現在のデータベース製品情報をGetProductByProductID(productID) インスタンスに読み取ります
  2. 手順 1 の ProductRow インスタンスに新しい値を割り当てる
  3. TableAdapter の Update メソッドを呼び出し、 ProductRow インスタンスを渡します

ただし、この一連の手順では、オプティミスティック コンカレンシーが正しくサポートされません。これは、手順 1 で設定された ProductRow がデータベースから直接入力されるためです。つまり、DataRow によって使用される元の値は、編集プロセスの開始時に GridView にバインドされた値ではなく、データベースに現在存在する値です。 代わりに、オプティミスティック コンカレンシーが有効な DAL を使用する場合は、次の手順を使用するように UpdateProduct メソッドのオーバーロードを変更する必要があります。

  1. TableAdapter の ProductsOptimisticConcurrencyRow メソッドを使用して、現在のデータベース製品情報をGetProductByProductID(productID) インスタンスに読み取ります
  2. 手順 1 の インスタンスにProductsOptimisticConcurrencyRowの値を割り当てる
  3. ProductsOptimisticConcurrencyRow インスタンスの AcceptChanges() メソッドを呼び出します。これにより、現在の値が "元の" 値であることを DataRow に指示します
  4. インスタンスにProductsOptimisticConcurrencyRow値を割り当てる
  5. TableAdapter の Update メソッドを呼び出し、 ProductsOptimisticConcurrencyRow インスタンスを渡します

手順 1 では、指定した製品レコードのすべての現在のデータベース値を読み取ります。 この手順は、UpdateProduct製品列を更新するオーバーロードでは余分ですが (これらの値は手順 2 で上書きされるため)、列値のサブセットのみが入力パラメーターとして渡されるオーバーロードには不可欠です。 元の値が ProductsOptimisticConcurrencyRow インスタンスに割り当てられると、AcceptChanges() メソッドが呼び出され、現在の DataRow 値が、@original_ColumnName ステートメントのUPDATE パラメーターで使用される元の値としてマークされます。 次に、新しいパラメーター値が 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 ページを構成する必要があります。

まず、OptimisticConcurrency.aspx フォルダー内のEditInsertDelete ページを開き、デザイナーに GridView を追加し、そのIDプロパティを ProductsGrid に設定します。 GridView のスマート タグから、 ProductsOptimisticConcurrencyDataSourceという名前の新しい ObjectDataSource を作成することを選択します。 この ObjectDataSource でオプティミスティック コンカレンシーをサポートする DAL を使用する必要があるため、 ProductsOptimisticConcurrencyBLL オブジェクトを使用するように構成します。

ObjectDataSource で ProductsOptimisticConcurrencyBLL オブジェクトを使用する

図 13: ObjectDataSource に ProductsOptimisticConcurrencyBLL オブジェクトを使用させる (フルサイズの画像を表示する をクリックします)

ウィザードのドロップダウン リストから、 GetProductsUpdateProduct、および 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_productNameoriginal_supplierIDなどの名前を付けたので、 OldValuesParameterFormatString プロパティの値は original_{0}のままにしておくことができます。 ただし、BLL メソッドの入力パラメーターに old_productNameold_supplierIDなどの名前がある場合は、 OldValuesParameterFormatString プロパティを old_{0}に更新する必要があります。

ObjectDataSource が元の値を BLL メソッドに正しく渡すには、最終的なプロパティ設定を 1 つ行う必要があります。 ObjectDataSource には、次の 2 つの値のいずれかに割り当てることができる ConflictDetection プロパティがあります。

  • OverwriteChanges - 既定値。は、元の値を BLL メソッドの元の入力パラメーターに送信しません
  • CompareAllValues - 元の値を BLL メソッドに送信します。オプティミスティック コンカレンシーを使用する場合は、このオプションを選択します

ConflictDetection プロパティを CompareAllValues に設定します。

GridView のプロパティとフィールドの構成

ObjectDataSource のプロパティが正しく構成された状態で、GridView の設定に注目しましょう。 まず、GridView で編集と削除をサポートする必要があるため、GridView のスマート タグから [編集を有効にする] チェック ボックスと [削除を有効にする] チェック ボックスをクリックします。 これにより、 ShowEditButtonShowDeleteButton の両方が trueに設定されている CommandField が追加されます。

ProductsOptimisticConcurrencyDataSource ObjectDataSource にバインドされたとき、GridView には各プロダクトのデータフィールドが含まれます。 このような GridView は編集可能ですが、ユーザー エクスペリエンスは到底許容できるものではありません。 CategoryIDおよび SupplierID BoundFields は TextBox としてレンダリングされ、ユーザーは適切なカテゴリとサプライヤーを ID 番号として入力する必要があります。 数値フィールドの書式設定が行われず、製品名が提供されていることや、単価、在庫数、注文数、再発注水準の値が適切な数値であり、0以上であることを確認する検証コントロールもありません。

編集および挿入インターフェイスへの検証コントロールの追加」および「データ変更インターフェイスのカスタマイズ」チュートリアルで説明したように、BoundFields を TemplateFields に置き換えることで、ユーザー インターフェイスをカスタマイズできます。 私はこの GridView とその編集インターフェイスを次の方法で変更しました。

  • BoundFields の ProductIDSupplierNameCategoryName を削除しました
  • ProductName BoundField を TemplateField に変換し、RequiredFieldValidation コントロールを追加しました。
  • CategoryIDSupplierID BoundFields を TemplateFields に変換し、TextBoxes ではなく DropDownLists を使用するように編集インターフェイスを調整しました。 これらの TemplateFields の ItemTemplatesでは、 CategoryNameSupplierName のデータ フィールドが表示されます。
  • BoundFields の UnitPriceUnitsInStockUnitsOnOrderReorderLevel を 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 では、左端の列に [編集] ボタンと [削除] ボタンがある各製品が一覧表示されます。

製品が GridView に一覧表示される

図 14: 製品が GridView に一覧表示されます (フルサイズの画像を表示する をクリックします)。

いずれかの製品の [削除] ボタンをクリックすると、 FormatException が発生します。

製品を削除しようとすると 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を取得します。

ObjectDataSource は、送信する入力パラメーターを持つメソッドを見つけることができません

図 16: ObjectDataSource は、送信する入力パラメーターを持つメソッドを見つけることができません (フルサイズの画像を表示する をクリックします)。

例外のメッセージを見ると、ObjectDataSource が、DeleteProductoriginal_CategoryName入力パラメーターを含む BLL original_SupplierName メソッドを呼び出したいと考えているのは明らかです。 これは、ItemTemplateおよびCategoryID TemplateFields のSupplierIDには、現在、CategoryNameおよびSupplierNameデータ フィールドを含む双方向の Bind ステートメントが含まれているためです。 代わりに、BindCategoryIDのデータ フィールドに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 番目のブラウザー ウィンドウで、 UnitPrice25.00 に更新します。 オプティミスティック コンカレンシーのサポートがない場合、2 番目のブラウザー インスタンスで [更新] をクリックすると、製品名が "Chai" に戻り、最初のブラウザー インスタンスによって行われた変更が上書きされます。 ただし、オプティミスティック コンカレンシーが採用されている場合、2 番目のブラウザー インスタンスで [更新] ボタンをクリックすると 、DBConcurrencyException が発生します

コンカレンシー違反が検出されると、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." />

VisibleEnabledViewState、およびTextプロパティを設定することに加えて、CssClassプロパティをWarningに設定しました。これにより、ラベルは大きな赤、斜体、太字のフォントで表示されます。 この CSS Warning クラスは、 挿入、更新、および削除に関連するイベントの検査に関する チュートリアルのStyles.cssに定義および追加されました。

これらのラベルを追加すると、Visual Studio のデザイナーは図 18 のようになります。

2 つのラベル コントロールがページに追加されました

図 18: 2 つのラベル コントロールがページに追加されました (フルサイズの画像を表示する をクリックします)。

これらの Label Web コントロールが配置されたので、コンカレンシー違反がいつ発生したかを判断する方法を調べる準備ができました。その時点で、適切な Label の Visible プロパティを trueに設定して、情報メッセージを表示できます。

更新時のコンカレンシー違反の処理

最初に、バッチ更新パターンを使用するときにコンカレンシー違反を処理する方法を見てみましょう。 バッチ更新パターンでこのような違反が発生すると、 DBConcurrencyException 例外がスローされるため、ASP.NET ページにコードを追加して、更新プロセス中に DBConcurrencyException 例外が発生したかどうかを判断する必要があります。 その場合は、別のユーザーがレコードの編集を開始してから [更新] ボタンをクリックしてから同じデータを変更したため、変更が保存されなかったことを説明するメッセージをユーザーに表示する必要があります。

ASP.NET ページのチュートリアルの BLL 例外と DAL-Level 例外の処理に 関するチュートリアルで説明したように、このような例外は、データ Web コントロールのポストレベル イベント ハンドラーで検出および抑制できます。 したがって、GridView のRowUpdated イベントに対して、DBConcurrencyException 例外がスローされたかどうかを確認するイベント ハンドラーを作成する必要があります。 このイベント ハンドラーは、次のイベント ハンドラー コードに示すように、更新プロセス中に発生したすべての例外への参照を渡します。

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 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 ではなく、ブール値であることを確認する必要があります。 このチェックに合格した場合、DeleteConflictMessageReturnValueされている場合は、false Label コントロールが表示されます。 これは、次のコードを使用して実現できます。

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 は、独立したコンサルタント、トレーナー、ライターとして働いています。 彼の最新の本は サムズ・ティーチ・セルフ ASP.NET 24時間で2.0です。 彼には mitchell@4GuysFromRolla.comで連絡できます。