オプティミスティック同時実行制御を実装する (C#)

作成者: Scott Mitchell

PDF のダウンロード

複数のユーザーがデータを編集できる Web アプリケーションの場合、2 人のユーザーが同時に同じデータを編集している可能性があります。 このチュートリアルでは、このリスクを処理するためのオプティミスティック コンカレンシー制御を実装します。

はじめに

ユーザーのみがデータを表示できる Web アプリケーション、またはデータを変更できる 1 人のユーザーのみを含む Web アプリケーションの場合、2 人の同時ユーザーが誤って相互の変更を上書きする脅威はありません。 ただし、複数のユーザーがデータを更新または削除できる Web アプリケーションの場合、あるユーザーの変更が別の同時ユーザーと競合する可能性があります。 コンカレンシー ポリシーが設定されていない場合、2 人のユーザーが同時に 1 つのレコードを編集している場合、変更を最後にコミットしたユーザーは、最初のユーザーによって行われた変更をオーバーライドします。

たとえば、Jisun と Sam の 2 人のユーザーが、ユーザーが GridView コントロールを使用して製品を更新および削除できるアプリケーションのページにアクセスしていたとします。 どちらの場合も、GridView の [編集] ボタンを同時にクリックします。 Jisun は製品名を "Chai Tea" に変更し、[更新] ボタンをクリックします。 結果は UPDATE データベースに送信されるステートメントです。これにより、製品 のすべての 更新可能なフィールドが設定されます (Jisun が更新したフィールド ProductNameは 1 つだけですが)。 この時点で、データベースには"Chai Tea"、カテゴリ飲料、サプライヤーエキゾチックリキッドなどの値があります。 ただし、Sam の画面の GridView には、編集可能な GridView 行に "Chai" という製品名が引き続き表示されます。 Jisun の変更がコミットされてから数秒後、Sam はカテゴリを Condiments に更新し、[更新] をクリックします。 その結果、製品名を UPDATE "Chai" CategoryID に設定するステートメントがデータベースに送信され、対応する Beverages カテゴリ ID に が設定されます。 製品名に対する Jisun の変更が上書きされました。 図 1 は、この一連のイベントをグラフィカルに示しています。

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

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

同様に、2 人のユーザーがページにアクセスしている場合、あるユーザーが別のユーザーによって削除されたときにレコードを更新中である可能性があります。 または、ユーザーがページを読み込んでから [削除] ボタンをクリックすると、別のユーザーがそのレコードの内容を変更している可能性があります。

使用できる コンカレンシー制御 戦略は 3 つあります。

  • Do Nothing -同時実行ユーザーが同じレコードを変更している場合は、最後のコミットを優先させます (既定の動作)
  • オプティミスティック コンカレンシー - コンカレンシーの競合が発生する可能性がありますが、そのような競合の大部分は発生しないと仮定します。したがって、競合が発生した場合は、別のユーザーが同じデータを変更したために変更を保存できないことをユーザーに通知するだけです
  • ペシミスティック コンカレンシー - コンカレンシーの競合が一般的であり、別のユーザーの同時実行アクティビティのために変更が保存されなかったとユーザーが伝えられるのを許容しないと仮定します。そのため、あるユーザーがレコードの更新を開始するとロックされるため、ユーザーが変更をコミットするまで、他のユーザーがそのレコードを編集または削除できなくなります。

ここまでのすべてのチュートリアルでは、既定のコンカレンシー解決戦略を使用してきました。つまり、最後の書き込みが優先されました。 このチュートリアルでは、オプティミスティック コンカレンシー制御を実装する方法について説明します。

注意

このチュートリアル シリーズでは、ペシミスティック コンカレンシーの例については説明しません。 このようなロックが適切に放棄されていない場合、他のユーザーがデータを更新できなくなる可能性があるため、ペシミスティックコンカレンシーはほとんど使用されません。 たとえば、ユーザーが編集のためにレコードをロックし、ロックを解除する前の日に終了した場合、元のユーザーが戻って更新を完了するまで、他のユーザーはそのレコードを更新できません。 したがって、ペシミスティックコンカレンシーが使用される状況では、通常、タイムアウトが発生し、到達した場合はロックが取り消されます。 チケット販売 Web サイトは、ユーザーが注文プロセスを完了している間に特定の座席の場所を短期間ロックする、ペシミスティックコンカレンシー制御の例です。

手順 1: オプティミスティック コンカレンシーの実装方法を確認する

オプティミスティック コンカレンシー制御は、更新または削除されるレコードの値が、更新または削除プロセスの開始時と同じであることを確認することによって機能します。 たとえば、編集可能な GridView で [編集] ボタンをクリックすると、レコードの値がデータベースから読み取られ、TextBoxes やその他の Web コントロールに表示されます。 これらの元の値は GridView によって保存されます。 その後、ユーザーが変更を加えて [更新] ボタンをクリックすると、元の値と新しい値がビジネス ロジック層に送信され、データ アクセス層に送信されます。 データ アクセス層は、ユーザーが編集を開始した元の値がデータベース内の値と同じ場合にのみレコードを更新する SQL ステートメントを発行する必要があります。 図 2 は、この一連のイベントを示しています。

Update または Delete を成功させるには、元の値が現在のデータベース値と等しい必要があります

図 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

注意

この UPDATE ステートメントは、読みやすくするために簡略化されています。 実際には、 句のUnitPriceWHEREチェックは、 を含NULLみ、常に False を返すかどうかをNULL = NULL確認できるためUnitPrice、より複雑になります (代わりに を使用IS NULLする必要があります)。

別の基になる UPDATE ステートメントを使用するだけでなく、オプティミスティック コンカレンシーを使用するように TableAdapter を構成すると、その DB ダイレクト メソッドのシグネチャも変更されます。 最初のチュートリアル「 データ アクセス層の作成」から、DB ダイレクト メソッドは、(厳密に型指定された DataRow または DataTable インスタンスではなく) 入力パラメーターとしてスカラー値のリストを受け入れるメソッドであることを思い出してください。 オプティミスティック コンカレンシーを使用する場合、DB ダイレクト Update() メソッドと Delete() メソッドには、元の値の入力パラメーターも含まれます。 さらに、バッチ更新パターンを使用するための BLL 内のコード ( Update() スカラー値ではなく DataRows と DataTables を受け入れるメソッド オーバーロード) も変更する必要があります。

既存の DAL の TableAdapter を拡張してオプティミスティック コンカレンシーを使用するのではなく (対応するために BLL を変更する必要があります)、代わりに という名前 NorthwindOptimisticConcurrencyの新しい型指定された DataSet を作成し、オプティミスティック コンカレンシーを使用する TableAdapter を追加 Products します。 その後、オプティミスティック コンカレンシー DAL をサポートするための適切な変更を加えたビジネス ロジック レイヤー クラスを作成 ProductsOptimisticConcurrencyBLL します。 この基礎が整ったら、ASP.NET ページを作成する準備が整います。

手順 2: オプティミスティック コンカレンシーをサポートするデータ アクセス層を作成する

新しい型指定された DataSet を作成するには、フォルダー内App_CodeのフォルダーをDAL右クリックし、 という名前NorthwindOptimisticConcurrencyの新しい DataSet を追加します。 最初のチュートリアルで説明したように、これにより、新しい TableAdapter が型指定された DataSet に追加され、TableAdapter 構成ウィザードが自動的に起動されます。 最初の画面では、接続するデータベースを指定するように求められます。 の Web.config設定を使用して、同じ Northwind データベースにNORTHWNDConnectionString接続します。

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

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

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

アドホック SQL ステートメントを使用して取得するデータを指定する

図 4: アドホック SQL ステートメントを使用して取得するデータを指定する (フルサイズの画像を表示する をクリックします)

次の画面で、製品情報の取得に使用する SQL クエリを入力します。 元の DAL から TableAdapter に使用されるのとまったく同じ SQL クエリを使用 Products しましょう。このクエリでは、すべての列が製品の 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 で TableAdapter から Products 同じ SQL クエリを使用する (フルサイズの画像を表示するにはクリックします)

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

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

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

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

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

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

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

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

図 8: DataTable と TableAdapter が型指定された DataSet に追加されました (フルサイズの画像を表示する をクリックします)

TableAdapter (オプティミスティック コンカレンシーを使用) と Products TableAdapter (そうでない) の間ProductsOptimisticConcurrencyの クエリと DELETE クエリの違いUPDATEを確認するには、TableAdapter をクリックし、プロパティ ウィンドウに移動します。 DeleteCommandプロパティと UpdateCommand プロパティのCommandTextサブプロパティでは、DAL の更新または削除関連のメソッドが呼び出されたときにデータベースに送信される実際の SQL 構文を確認できます。 TableAdapter の ProductsOptimisticConcurrency 場合、使用される 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))

元の DELETE DAL の Product TableAdapter のステートメントははるかに簡単です。

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

ご覧のように、オプティミスティック コンカレンシーを使用する TableAdapter の ステートメントの 句DELETEには、WHERE各テーブルの既存のProduct列値と、GridView (または DetailsView または FormView) が最後に設定された時点での元の値との比較が含まれています。 、、 以外ProductIDのすべてのフィールドには値を含NULLめることができるため、句の値を正しく比較NULLするための追加のパラメーターとチェックがWHEREDiscontinuedまれます。 ProductName

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

これを行うには、TableAdapter のタイトル バー (メソッド名と GetProducts メソッド名のすぐ上Fillの領域) を右クリックし、コンテキスト メニューから [クエリの追加] を選択します。 これにより、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: メソッドの名前を と GetProductByProductIDFillByProductID変更します (クリックするとフルサイズの画像が表示されます)

このウィザードが完了すると、TableAdapter には、データ GetProducts()を取得するための 2 つのメソッドが含まれるようになりました。このメソッドは 、すべての 製品を返し、 GetProductByProductID(productID)は指定した製品を返します。

手順 3: オプティミスティック Concurrency-Enabled DAL 用のビジネス ロジック レイヤーを作成する

既存 ProductsBLL のクラスには、バッチ更新パターンと DB ダイレクト パターンの両方を使用する例があります。 メソッドとUpdateProductオーバーロードはAddProductどちらもバッチ更新パターンを使用し、インスタンスを ProductRow TableAdapter の Update メソッドに渡します。 一方、 メソッドは DeleteProduct DB ダイレクト パターンを使用し、TableAdapter の Delete(productID) メソッドを呼び出します。

新しい ProductsOptimisticConcurrency TableAdapter では、DB ダイレクト メソッドで元の値も渡す必要があります。 たとえば、 メソッドでは、元の DeleteCategoryIDUnitsInStockUnitPriceDiscontinuedProductNameReorderLevelSupplierIDUnitsOnOrderQuantityPerUnit、 の 10 個の入力パラメーターが必要になりました。ProductID データベースに送信されたステートメントの 句で 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 に指示されます。

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

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

オプティミスティック コンカレンシーを使用する DAL に対して DB ダイレクト パターンを使用する場合、メソッドには新しい値と元の値を渡す必要があります。 削除の場合、新しい値がないため、元の値のみを渡す必要があります。 BLL では、元のすべてのパラメーターを入力パラメーターとして受け入れる必要があります。 クラスの メソッドで DeleteProductProductsOptimisticConcurrencyBLL 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 GetProductByProductID(productID) の メソッドを使用して、ProductRow現在のデータベース製品情報をインスタンスに読み取る
  2. 手順 1 で新しい値をインスタンスに ProductRow 割り当てる
  3. インスタンスを渡して TableAdapter の Update メソッドを ProductRow 呼び出す

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

  1. TableAdapter GetProductByProductID(productID) の メソッドを使用して、ProductsOptimisticConcurrencyRow現在のデータベース製品情報をインスタンスに読み取る
  2. 手順 1 で の値をインスタンスに ProductsOptimisticConcurrencyRow 割り当てる
  3. インスタンスの AcceptChanges() メソッドをProductsOptimisticConcurrencyRow呼び出します。このメソッドは、現在の値が "元の" 値であることを DataRow に指示します
  4. 新しい値をインスタンスにProductsOptimisticConcurrencyRow割り当てる
  5. インスタンスを渡して 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 ページを構成する必要があります。

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

ObjectDataSource で 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コレクションには、クラスの メソッド内の 10 個の入力パラメーターのそれぞれに対するインスタンスがProductsOptimisticConcurrencyBLLDeleteProduct含まれていますParameter。 同様に UpdateParameters 、コレクションには、 Parameter の各入力パラメーター UpdateProductのインスタンスが含まれます。

データの変更に関連するこれらのチュートリアルでは、この時点で ObjectDataSource の OldValuesParameterFormatString プロパティを削除します。このプロパティは、BLL メソッドが古い (または元の) 値と新しい値が渡されることを想定しているためです。 さらに、このプロパティ値は、元の値の入力パラメーター名を示します。 元の値を BLL に渡しているため、このプロパティを削除 しないでください

注意

プロパティの値は、 OldValuesParameterFormatString 元の値を想定する BLL の入力パラメーター名にマップする必要があります。 これらのパラメーター original_productName、、 original_supplierIDなどという名前を付けたので、 プロパティの値は OldValuesParameterFormatString のままに original_{0}しておくことができます。 ただし、BLL メソッドの入力パラメーターに 、、などのold_productName名前がある場合は、 プロパティを OldValuesParameterFormatStringold_{0}更新する必要old_supplierIDがあります。

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

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

少し時間を取って、 プロパティを にConflictDetectionCompareAllValues設定します。

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

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

ObjectDataSource に ProductsOptimisticConcurrencyDataSource バインドすると、GridView には製品の各データ フィールドのフィールドが含まれます。 このような GridView は編集できますが、ユーザー エクスペリエンスは許容される以外の何物でもかまいません。 CategoryIDおよび SupplierID BoundFields は TextBox としてレンダリングされ、ユーザーは適切なカテゴリとサプライヤーを ID 番号として入力する必要があります。 数値フィールドの書式設定はなく、製品の名前が指定されていること、および単価、在庫単位、注文単位、並べ替えレベルの値が両方とも適切な数値であり、0 以上であることを確認するための検証コントロールはありません。

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

  • SupplierNameProductIDおよび CategoryName BoundFields を削除しました
  • BoundField を ProductName TemplateField に変換し、RequiredFieldValidation コントロールを追加しました。
  • CategoryIDおよび SupplierID BoundFields を TemplateFields に変換し、TextBoxes ではなく DropDownLists を使用するように編集インターフェイスを調整しました。 これらの TemplateFields' ItemTemplatesでは、 CategoryName フィールドと SupplierName データ フィールドが表示されます。
  • UnitsInStockUnitPriceUnitsOnOrder、および 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>

完全に機能する例を作成することに非常に近いです。 しかし、いくつかの微妙な点があり、それが忍び寄って問題を引き起こします。 さらに、コンカレンシー違反が発生したときにユーザーに警告するインターフェイスも必要です。

注意

データ 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として書式設定を表示しますが、これを実現するにはキーワード (keyword)を使用Evalします。 では、一方向のデータ バインドが実行されることを Eval 思い出してください。 元の値の値を UnitPrice 指定する必要があるため、 には双方向の databinding ステートメント ItemTemplateが必要ですが、これは、 プロパティが に設定されている Visible Label Web コントロールに false配置できます。 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 のイベント ハンドラーで、値が表示される Label Web コントロール UnitPrice にプログラムでアクセスし、そのプロパティを Text 書式設定されたバージョンに設定します。
  • 書式は UnitPrice 通貨のままにします。 GridView のイベント ハンドラーで、 を使用してDecimal.Parse、既存の元UnitPriceRowDeleting値 ($19.95) を実際の 10 進値に置き換えます。 イベント ハンドラーで RowUpdating 同様のことを行う方法については、ASP.NET ページのチュートリアルの BLL とDAL-Level例外の処理に関するチュートリアルを 参照してください。

私の例では、2番目のアプローチで、プロパティが Text 書式設定されていない値にバインドされた双方向データである非表示の Label Web コントロールを追加することを選択しました UnitPrice

この問題を解決したら、製品の [削除] ボタンをもう一度クリックしてみてください。 今回は、ObjectDataSource が BLL UpdateProduct のメソッドを呼び出そうとしたときに を取得InvalidOperationExceptionします。

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

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

例外のメッセージを見ると、ObjectDataSource が および original_SupplierName 入力パラメーターを含む original_CategoryName BLL DeleteProduct メソッドを呼び出したいと考えているのは明らかです。 これは、現在、 ItemTemplate および SupplierID TemplateFields の CategoryID s には、 および データ フィールドを含む双方向 Bind ステートメントがCategoryNameSupplierName含まれているためです。 代わりに、 および SupplierID データ フィールドを含むステートメントをCategoryID含めるBind必要があります。 これを実現するには、既存の Bind ステートメントを ステートメントにEval置き換え、次に示すように、双方向データ バインドを使用して プロパティが と SupplierID データ フィールドにCategoryIDバインドされている非表示の Label コントロールTextを追加します。

<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 は元の値を渡してクラスのDeleteProductメソッドをProductsOptimisticConcurrencyBLL呼び出します。 2 番目のブラウザー インスタンスの元 ProductName の値は "Chai Tea" であり、データベース内の現在 ProductName の値と一致しません。 したがって、 句が DELETE 満たすレコードがデータベースにないため、データベースに対して発行されたステートメントは 0 行に WHERE 影響します。 メソッドは DeleteProductfalse 返し、ObjectDataSource のデータは GridView にリバウンドされます。

エンド ユーザーの観点から、2 つ目のブラウザー ウィンドウで Chai Tea の [削除] ボタンをクリックすると、画面が点滅し、製品は "Chai" (最初のブラウザー インスタンスによって行われた製品名の変更) として表示されますが、画面が点滅し、製品はまだそこに残っています。 ユーザーがもう一度 [削除] ボタンをクリックすると、GridView の元 ProductName の値 ("Chai") がデータベース内の値と一致するようになりましたので、削除は成功します。

どちらの場合も、ユーザー エクスペリエンスは理想的とはほど遠いです。 バッチ更新パターンを使用するときに、例外の nitty-gritty の詳細を DBConcurrencyException ユーザーに表示することは明らかに望んでいません。 また、DB ダイレクト パターンを使用する場合の動作は、users コマンドが失敗したときにやや混乱しますが、その理由を正確に示すものはありませんでした。

これら 2 つの問題を解決するために、ページにラベル Web コントロールを作成し、更新または削除が失敗した理由を説明します。 バッチ更新パターンでは、GridView の事後レベルのイベント ハンドラーで例外が発生したかどうかを DBConcurrencyException 判断し、必要に応じて警告ラベルを表示します。 DB ダイレクト メソッドの場合は、BLL メソッドの戻り値 ( true 1 行が影響を受けた場合、それ以外の場合) を調べ、 false 必要に応じて情報メッセージを表示できます。

手順 6: 情報メッセージを追加し、コンカレンシー違反が発生した場合に表示する

コンカレンシー違反が発生した場合に発生する動作は、DAL のバッチ更新と DB ダイレクト パターンのどちらを使用したかによって異なります。 このチュートリアルでは、更新に使用されるバッチ更新パターンと削除に使用される DB ダイレクト パターンの両方のパターンを使用します。 まず、データを削除または更新しようとしたときにコンカレンシー違反が発生したことを説明する 2 つの Label Web コントロールをページに追加しましょう。 Label コントロールVisibleEnableViewState プロパティを にfalse設定します。これにより、プロパティがプログラムによって に設定trueされている特定のページ アクセスを除き、各ページ アクセスVisibleで非表示になります。

<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." />

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

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

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

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

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

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

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

ASP.NET Page での 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 、このイベント ハンドラーは Label コントロールを UpdateConflictMessage 表示し、例外が処理されたことを示します。 このコードを設定すると、レコードの更新時にコンカレンシー違反が発生すると、ユーザーの変更は失われます。これは、別のユーザーの変更が同時に上書きされるためです。 特に、GridView は編集前の状態に戻され、現在のデータベース データにバインドされます。 これにより、GridView 行が、以前は表示されなかった他のユーザーの変更で更新されます。 さらに、 UpdateConflictMessage Label コントロールは、発生した内容をユーザーに説明します。 この一連のイベントの詳細については、図 19 を参照してください。

コンカレンシー違反が発生した場合にユーザーの更新が失われる

図 19: コンカレンシー違反の顔でユーザーの更新が失われました (フルサイズの画像を表示する をクリックします)

注意

または、GridView を編集前の状態に戻す代わりに、渡されたGridViewUpdatedEventArgsオブジェクトの プロパティを true に設定KeepInEditModeすることで、GridView を編集状態のままにすることもできます。 ただし、この方法を使用する場合は、他のユーザーの値が編集インターフェイスに読み込まれるように、(メソッド DataBind() を呼び出して) データを GridView に再バインドしてください。 このチュートリアルでダウンロードできるコードには、イベント ハンドラーに RowUpdated これら 2 行のコードがコメントアウトされています。これらのコード行のコメントを解除するだけで、コンカレンシー違反の後も GridView を編集モードのままにします。

削除時のコンカレンシー違反への対応

DB ダイレクト パターンでは、コンカレンシー違反が発生しても例外は発生しません。 代わりに、WHERE 句はどのレコードとも一致しないため、データベース ステートメントは単にレコードに影響しません。 BLL で作成されたすべてのデータ変更メソッドは、1 つのレコードに影響を与えたかどうかを示すブール値を返すように設計されています。 したがって、レコードを削除するときにコンカレンシー違反が発生したかどうかを判断するために、BLL DeleteProduct の メソッドの戻り値を調べることができます。

BLL メソッドの戻り値は、イベント ハンドラーに渡されるオブジェクトの プロパティを使用して ReturnValue 、ObjectDataSource の事後レベルの ObjectDataSourceStatusEventArgs イベント ハンドラーで調べることができます。 メソッドから DeleteProduct 戻り値を決定することに関心があるため、ObjectDataSource Deleted のイベントのイベント ハンドラーを作成する必要があります。 プロパティは ReturnValueobject であり、例外が発生し、値を返す前にメソッドが中断された場合に、 を指定できます null 。 したがって、まず、 プロパティが ではなくnull、ブール値であることをReturnValue確認する必要があります。 このチェックが渡されると仮定すると、 が falseの場合は DeleteConflictMessage Label コントロールがReturnValue表示されます。 これを行うには、次のコードを使用します。

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見つけることができます。