次の方法で共有


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

作成者: Scott Mitchell

PDF のダウンロード

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

はじめに

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

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

When Two Users Simultaneously Update a Record There s Potential for One User s Changes to Overwrite the Other s

図 1: 2 人のユーザーがレコードを同時に更新する場合、一方のユーザーの変更によって、もう一方の変更が上書きされることがある (クリックするとフルサイズの画像が表示されます)

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

使用できる同時実行制御戦略は 3 つあります。

  • 何もしない - ユーザーが同じレコードを同時に変更している場合、最後のコミットを優先させます (既定の動作)
  • オプティミスティック同時実行制御 - コンカレンシーの競合が発生することがありますが、このような競合が発生しない時間が大半を占めることを前提としています。このため、競合が発生した場合は、他のユーザーが同じデータを変更しているため変更を保存できない旨を、ただそのユーザーに通知します
  • ペシミスティック同時実行制御 - コンカレンシーの競合が日常的に発生し、別のユーザーによる同時アクティビティが原因で変更が保存されなかったと言われることが許容されないことを前提としています。このため、あるユーザーがレコードの更新を開始したら、そのレコードをロックし、そのユーザーが変更をコミットするまで他のユーザーがそのレコードを編集または削除できないようにします

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

Note

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

手順 1: オプティミスティック同時実行制御の実装方法を確認する

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

For the Update or Delete to Succeed, the Original Values Must Be Equal to the Current Database Values

図 2: 更新または削除を成功させるには、元の値が現在のデータベースの値と同じでなければならない (クリックするとフルサイズの画像が表示されます)

オプティミスティック同時実行制御を実装するには、さまざまな方法があります (オプションをいくつか簡単に確認するには、Peter A. Brombergオプティミスティック同時実行制御の更新ロジックに関するページを参照してください)。 ADO.NET 型指定された DataSet には、チェック ボックスをオンにするだけで構成できる実装が 1 つ用意されています。 型指定された DataSet で TableAdapter のオプティミスティック同時実行制御を有効にすると、TableAdapter の UPDATE ステートメントと DELETE ステートメントが拡張され、WHERE 句の元の値すべての比較が含められます。 たとえば、次の UPDATE ステートメントは、現在のデータベース値が、GridView のレコードを更新するときに最初に取得された値と等しい場合にのみ、製品の名前と価格を更新します。 @ProductName および @UnitPrice パラメーターには、ユーザーが入力した新しい値が含まれます。一方、@original_ProductName@original_UnitPrice には、[編集] ボタンがクリックされたときに GridView に最初に読み込まれた値が含まれます。

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

Note

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

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

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

手順 2: オプティミスティック同時実行制御をサポートするデータ アクセス層を作成する

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

Connect to the Same Northwind Database

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

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

Specify the Data to Retrieve Using an Ad-Hoc SQL Statement

図 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

Use the Same SQL Query from the Products TableAdapter in the Original DAL

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

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

Enable Optimistic Concurrency Control by Checking the

図 6: [オプティミスティック同時実行制御を使用する] チェック ボックスをオンにしてオプティミスティック同時実行制御を有効にする (クリックするとフルサイズの画像が表示されます)

最後に、TableAdapter が使用するデータ アクセス パターンでは、DataTable にデータが格納され、DataTable が返されることを指定します。また、DB ダイレクト メソッドを作成する必要があることも指定します。 "DataTable を返す" パターンのメソッド名を GetData から GetProducts に変更して、元の DAL で使用した名前付け規則を反映させます。

Have the TableAdapter Utilize All Data Access Patterns

図 7: TableAdapter ですべてのデータ アクセス パターンが使用されるようにする (クリックするとフルサイズの画像が表示されます)

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

A DataTable and TableAdapter Have Been Added to the Typed DataSet

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

ProductsOptimisticConcurrency TableAdapter (オプティミスティック同時実行制御を使用) と Products TableAdapter (その制御を使用していない) の間の UPDATE クエリと DELETE クエリの違いを確認するには、TableAdapter をクリックして、プロパティ ウィンドウに移動します。 DeleteCommand および UpdateCommand プロパティの CommandText サブプロパティで、DAL の更新または削除関連のメソッドが呼び出されたときにデータベースに送信される実際の SQL 構文を確認できます。 ProductsOptimisticConcurrency TableAdapter の場合、次の DELETE ステートメントが使用されます。

DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

元の DAL の Product TableAdapter に対する DELETE ステートメントは、はるかにシンプルです。

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

ご覧のように、オプティミスティック同時実行制御を使用する TableAdapter に対する DELETE ステートメントの WHERE 句には、Product テーブルの既存の列の各値と、GridView (または DetailsView あるいは FormView) が最後に設定された時点での元の値との比較が含まれます。 ProductIDProductNameDiscontinued 以外のすべてのフィールドに NULL 値が含まれる可能性があるため、WHERE 句 の NULL 値を正しく比較するために、追加のパラメーターとチェックが含まれます。

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

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

Mark the Query Type as a

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

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

Add a WHERE Clause to the Pre-Loaded Query to Return a Specific Product Record

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

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

Rename the Methods to FillByProductID and GetProductByProductID

図 11: メソッドの名前を FillByProductID および GetProductByProductID に変更する (クリックするとフルサイズの画像が表示されます)

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

手順 3: オプティミスティック同時実行制御対応 DAL に対してビジネス ロジック層を作成する

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

新しい ProductsOptimisticConcurrency TableAdapter では、DB ダイレクト メソッドで元の値も渡す必要があります。 たとえば、Delete メソッドには、10 個の入力パラメーターとして元の ProductIDProductNameSupplierIDCategoryIDQuantityPerUnitUnitPriceUnitsInStockUnitsOnOrderReorderLevelDiscontinued が必要です。 これらの追加の入力パラメーターの値は、データベースに送信される DELETE ステートメントの WHERE 句で使用され、データベースの現在の値が元の値にマップされている場合にのみ、指定されたレコードを削除します。

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

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

Add the ProductsOptimisticConcurrencyBLL Class to the BLL Folder

図 12: ProductsOptimisticConcurrencyBLL クラスを BLL フォルダーに追加する

次に、ProductsOptimisticConcurrencyBLL クラスに次のコードを追加します。

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

クラス宣言の先頭の using NorthwindOptimisticConcurrencyTableAdapters ステートメントに注意してください。 NorthwindOptimisticConcurrencyTableAdapters 名前空間には、DAL のメソッドを提供する ProductsOptimisticConcurrencyTableAdapter クラスが含まれています。 また、クラス宣言の前には System.ComponentModel.DataObject 属性があります。この属性は、Visual Studio に対して、ObjectDataSource ウィザードのドロップダウン リストにこのクラスを含めるよう指示します。

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

オプティミスティック同時実行制御で DB ダイレクト パターンを使用して製品を削除する

オプティミスティック同時実行制御を使用する DAL に対して DB ダイレクト パターンを使用する場合は、メソッドに新しい値と元の値を渡す必要があります。 削除する場合、新しい値は存在しないため、渡す必要があるのは元の値のみです。 BLL では、元のパラメーターすべてを入力パラメーターとして受け入れる必要があります。 ProductsOptimisticConcurrencyBLL クラス内の DeleteProduct メソッドで DB ダイレクト メソッドが使用されるようにしてみましょう。 つまり、このメソッドは、次のコードに示すように、10 個すべての製品データ フィールドを入力パラメーターとして取り込み、DAL に渡す必要があります。

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = Adapter.Delete(original_productID,
                                      original_productName,
                                      original_supplierID,
                                      original_categoryID,
                                      original_quantityPerUnit,
                                      original_unitPrice,
                                      original_unitsInStock,
                                      original_unitsOnOrder,
                                      original_reorderLevel,
                                      original_discontinued);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

元の値、つまり GridView (または DetailsView あるいは FormView) に最後に読み込まれた値と、ユーザーが [削除] ボタンをクリックしたときのデータベースの値と異なる場合、WHERE 句はどのデータベース レコードとも一致せず、レコードへの影響はありません。 そのため、TableAdapter の Delete メソッドは 0 を返し、BLL の DeleteProduct メソッドは false を返します。

オプティミスティック同時実行制御でバッチ更新パターンを使用して製品を更新する

前述のように、バッチ更新パターンに対する TableAdapter の Update メソッドのメソッド シグネチャは、オプティミスティック同時実行制御が使用されているかどうかに関係なく同じです。 つまり、Update メソッドでは、DataRow、DataRows の配列、DataTable、または型指定された DataSet が必要です。 元の値を指定するための追加の入力パラメーターはありません。 これが可能なのは、DataTable が、その DataRow の元の値と変更された値を追跡しているためです。 DAL がその UPDATE ステートメントを発行すると、@original_ColumnName パラメーターには DataRow の元の値が設定され、@ColumnName パラメーターには DataRow の変更された値が設定されます。

ProductsBLLクラス (元の非オプティミスティック同時実行制御 DAL を使用) で、バッチ更新パターンを使用して製品情報を更新する場合、コードでは、次の一連のイベントが実行されます。

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

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

  1. TableAdapter の GetProductByProductID(productID) メソッドを使用して、現在のデータベース製品情報を ProductsOptimisticConcurrencyRow インスタンスに読み込みます
  2. 手順 1 の ProductsOptimisticConcurrencyRow インスタンスに "元の" 値を割り当てます
  3. ProductsOptimisticConcurrencyRow インスタンスの AcceptChanges() メソッドを呼び出し、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 ページは、コンカレンシー違反を適切に処理するように構成しなければなりません。

まず、EditInsertDelete フォルダー内の OptimisticConcurrency.aspx ページを開き、GridView をデザイナーに追加し、その ID プロパティを ProductsGrid に設定します。 GridView のスマート タグから、ProductsOptimisticConcurrencyDataSource という名前の新しい ObjectDataSource を作成します。 この ObjectDataSource は、オプティミスティック同時実行制御をサポートする DAL を使うように設定したいため、ProductsOptimisticConcurrencyBLL オブジェクトを使用するように構成します。

Have the ObjectDataSource Use the ProductsOptimisticConcurrencyBLL Object

図 13: ProductsOptimisticConcurrencyBLL オブジェクトを使用するように ObjectDataSource を設定する (クリックするとフルサイズの画像が表示されます)

ウィザードのドロップダウン リストから 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 コレクションには、ProductsOptimisticConcurrencyBLL クラスの DeleteProduct メソッド内の 10 個の入力パラメーターごとに Parameter インスタンスが含まれています。 同様に、UpdateParameters コレクションには、UpdateProduct の入力パラメーターごとに Parameter インスタンスが含まれています。

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

Note

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

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

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

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

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

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

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

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

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

これらのタスクを実行する方法については、これまでのチュートリアルで既に確認しました。ここでは最後の宣言構文の一覧を示すだけにとどめ、実装は練習として残しておきます。

<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
    OnRowUpdated="ProductsGrid_RowUpdated">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="EditProductName" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:TextBox>
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="EditProductName"
                    ErrorMessage="You must enter a product name."
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditCategoryID" runat="server"
                    DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
                    DataTextField="CategoryName" DataValueField="CategoryID"
                    SelectedValue='<%# Bind("CategoryID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetCategories" TypeName="CategoriesBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server"
                    Text='<%# Bind("CategoryName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditSuppliersID" runat="server"
                    DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
                    DataTextField="CompanyName" DataValueField="SupplierID"
                    SelectedValue='<%# Bind("SupplierID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label3" runat="server"
                    Text='<%# Bind("SupplierName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitPrice" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
                <asp:CompareValidator ID="CompareValidator1" runat="server"
                    ControlToValidate="EditUnitPrice"
                    ErrorMessage="Unit price must be a valid currency value without the
                    currency symbol and must have a value greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Currency"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label4" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsInStock" runat="server"
                    Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator2" runat="server"
                    ControlToValidate="EditUnitsInStock"
                    ErrorMessage="Units in stock must be a valid number
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label5" runat="server"
                    Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsOnOrder" runat="server"
                    Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator3" runat="server"
                    ControlToValidate="EditUnitsOnOrder"
                    ErrorMessage="Units on order must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label6" runat="server"
                    Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
            <EditItemTemplate>
                <asp:TextBox ID="EditReorderLevel" runat="server"
                    Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator4" runat="server"
                    ControlToValidate="EditReorderLevel"
                    ErrorMessage="Reorder level must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label7" runat="server"
                    Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

完全に動作する例になるまであと少しです。 しかし、やがて微妙な点がひそかに発生するようになり、それが問題を引き起こします。 さらに、コンカレンシー違反が発生したときにユーザーに警告するインターフェイスは、やはり必要です。

Note

データ Web コントロールが元の値を ObjectDataSource に正しく渡すには (これは、その後 BLL に渡されます)、GridView の EnableViewState プロパティが true (既定値) に設定されていることが重要です。 ビュー状態を無効にすると、元の値はポストバック時に失われます。

正しい元の値を ObjectDataSource に渡す

GridView の構成方法には問題がいくつかあります。 ObjectDataSource の ConflictDetection プロパティが CompareAllValues に設定されている場合 (当サイトと同様)、ObjectDataSource Update() または Delete() メソッドが GridView (または DetailsView あるいは FormView) によって呼び出されると、ObjectDataSource は、GridView の元の値を、その適切な Parameter インスタンスにコピーしようとします。 このプロセスをグラフィカルに表現したものについては、図 2 を参照してください。

具体的には、GridView の元の値には、データが GridView にバインドされるたびに、双方向のデータバインド ステートメントの値が割り当てられます。 したがって、必要な元の値はすべて、双方向のデータ バインドを使用してキャプチャされること、また、変換可能な形式で提供されることが重要です。

これが重要である理由を確認するには、ブラウザーでページにアクセスしてください。 予想どおり、GridView には、製品の一覧と、それぞれの左端の列に [編集] ボタンと [削除] ボタンが表示されます。

The Products are Listed in a GridView

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

いずれかの製品の [削除] ボタンをクリックすると、FormatException がスローされます。

Attempting to Delete Any Product Results in a FormatException

図 15: 製品を削除しようとすると FormatException が発生する (クリックするとフルサイズの画像が表示されます)

ObjectDataSource が元の UnitPrice 値を読み取ろうとすると、FormatException が発生します。 ItemTemplateUnitPrice は通貨 (<%# Bind("UnitPrice", "{0:C}") %>) として書式設定されているため、これには通貨記号が含まれます ($19.95 など)。 ObjectDataSource がこの文字列を decimal に変換しようとすると、FormatException が発生します。 この問題を回避するためのオプションはいくつかあります。

  • ItemTemplate から通貨書式を削除します。 つまり、<%# Bind("UnitPrice", "{0:C}") %> ではなく、単に <%# Bind("UnitPrice") %> を使用します。 この欠点は、価格が書式設定されなくなることです。
  • ItemTemplateUnitPrice を通貨として書式設定して表示しますが、Eval キーワードを使ってこれを行います。 Eval が一方向のデータ バインドを実行することを思い出してください。 引き続き元の値に対して UnitPrice 値を提供する必要があるため、ItemTemplate にはまだ双方向のデータバインド ステートメントが必要ですが、これは、Visible プロパティが false に設定されているラベル Web コントロールに配置することができます。 ItemTemplate では、次のマークアップを使用できます。
<ItemTemplate>
    <asp:Label ID="DummyUnitPrice" runat="server"
        Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
    <asp:Label ID="Label4" runat="server"
        Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
  • <%# Bind("UnitPrice") %> を使用して、ItemTemplate から通貨書式を削除します。 GridView の RowDataBound イベント ハンドラーで、ラベル Web コントロールにプログラムによってアクセスします。そのコントロールでは、UnitPrice 値が表示され、その Text プロパティが書式設定されたバージョンに設定されています。
  • UnitPrice の通貨としての書式設定をそのままにします。 GridView の RowDeleting イベント ハンドラーで、既存の元の UnitPrice 値 ($19.95) を、Decimal.Parse を使用して実際の 10 進値に置き換えます。 「ASP.NET ページで BLL レベルと DAL レベルの例外を処理する」チュートリアルでは、RowUpdating イベント ハンドラーで同様の操作を行う方法について確認しました。

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

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

The ObjectDataSource Cannot Find a Method with the Input Parameters it Wants to Send

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

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

<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummyCategoryID" runat="server"
            Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label2" runat="server"
            Text='<%# Eval("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummySupplierID" runat="server"
            Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label3" runat="server"
            Text='<%# Eval("SupplierName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

これらの変更により、製品情報を正常に削除および編集できるようになりました。 手順 5 では、コンカレンシー違反が検出されていることを確認する方法について説明します。 しかし、とりあえずは少し時間をとってレコードをいくつか更新および削除し、1 人のユーザーに対する更新と削除が想定どおりに動作することを確認してください。

手順 5: オプティミスティック同時実行制御サポートをテストする

(データがやみくもに上書きされるのではなく) コンカレンシー違反が検出されていることを確認するには、このページに対して 2 つのブラウザー ウィンドウを開く必要があります。 両方のブラウザー インスタンスで、Chai の [編集] ボタンをクリックします。 次に、ブラウザーの 1 つで、名前を "Chai Tea" に変更し、[更新] をクリックします。 更新が成功し、GridView が編集前の状態に戻り、"Chai Tea" が新しい製品名として表示されます。

ただし、もう 一方のブラウザー ウィンドウ インスタンスでは、まだ製品名 TextBox に "Chai" と表示されています。 この 2 番目のブラウザー ウィンドウで、UnitPrice25.00 に更新します。 オプティミスティック同時実行制御がサポートされていない場合、2 番目のブラウザー インスタンスで [更新] をクリックすると、製品名が "Chai" に戻り、最初のブラウザー インスタンスで行われた変更が上書きされます。 しかし、オプティミスティック同時実行制御が採用されている場合、2 番目のブラウザー インスタンスで [更新] ボタンをクリックすると、DBConcurrencyException が発生します。

When a Concurrency Violation is Detected, a DBConcurrencyException is Thrown

図 17: コンカレンシー違反が検出されると DBConcurrencyException がスローされる (クリックするとフルサイズの画像が表示されます)

DBConcurrencyException は、DAL のバッチ更新パターンが使用されている場合にのみスローされます。 DB ダイレクト パターンは例外を発生させるのではなく、影響を受けた行がないことを示すだけです。 これを示すために、両方のブラウザー インスタンスの GridView を編集前の状態に戻します。 次に、最初のブラウザー インスタンスで、[編集] ボタンをクリックし、製品名を "Chai Tea" から "Chai" に戻して、[更新] をクリックします。 2 番目のブラウザー ウィンドウで、Chai の [削除] ボタンをクリックします。

[削除] をクリックすると、ページはポストバックされ、GridView は ObjectDataSource の Delete() メソッドを呼び出し、ObjectDataSource は ProductsOptimisticConcurrencyBLL クラスの DeleteProduct メソッドを呼び出して元の値を渡します。 2 番目のブラウザー インスタンスの元の ProductName 値は "Chai Tea" であり、データベース内の現在の ProductName 値と一致しません。 そのため、データベースに対して発行された DELETE ステートメントの影響を受ける行はゼロです。WHERE 句を満たすレコードがデータベースに存在しないためです。 DeleteProduct メソッドは false を返し、ObjectDataSource のデータは GridView に再バインドされます。

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

どちらの場合も、ユーザー エクスペリエンスは理想的とは言えません。 バッチ更新パターンを使用する場合、当然 DBConcurrencyException 例外の肝心な詳細をユーザーには見せたくありません。 また、DB ダイレクト パターンを使用する場合の動作は、ユーザー コマンドが失敗したため、ややわかりにくくなっていますが、その理由を正確に示すものはありませんでした。

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

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

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

<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to delete has been modified by another user
           since you last visited this page. Your delete was cancelled to allow
           you to review the other user's changes and determine if you want to
           continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to update has been modified by another user
           since you started the update process. Your changes have been replaced
           with the current values. Please review the existing values and make
           any needed changes." />

VisibleEnabledViewStateText プロパティの設定のほか、CssClass プロパティを Warning に設定しました。これにより、ラベルが大きな赤、斜体、太字のフォントで表示されます。 この CSS Warning クラスは、「挿入、更新、削除に関連付けられているイベントを調べる」チュートリアルで定義され、Styles.css に追加されました。

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

Two Label Controls Have Been Added to the Page

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

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

更新時にコンカレンシー違反を処理する

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

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

protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

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

A User s Updates are Lost in the Face of a Concurrency Violation

図 19: コンカレンシー違反が発生した場合、ユーザーの更新が失われる (クリックするとフルサイズの画像が表示されます)

Note

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

削除時のコンカレンシー違反に対応する

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

BLL メソッドの戻り値は、イベント ハンドラーに渡された ObjectDataSourceStatusEventArgs オブジェクトの ReturnValue プロパティを使用して、ObjectDataSource のポストレベル イベント ハンドラーで調べることができます。 DeleteProduct メソッドからの戻り値を判断することが目的なので、ObjectDataSource の Deleted イベントのイベント ハンドラーを作成する必要があります。 ReturnValue プロパティは object 型で、例外が発生し、メソッドが値を返す前に中断された場合は null にすることができます。 したがって、まず、ReturnValue プロパティが null でないこと、またブール値であることを確認する必要があります。 このチェックが成功すると想定して、ReturnValuefalse の場合の DeleteConflictMessage ラベル コントロールを示します。 これを行うには、次のコードを使用します。

protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

コンカレンシー違反が発生した場合、ユーザーの削除要求は取り消されます。 GridView が更新され、ユーザーがページを読み込んでから [削除] ボタンをクリックするまでの間に、そのレコードに対して発生した変更が表示されます。 このような違反が発生すると、DeleteConflictMessage ラベルが表示され、発生した内容に関する説明が示されます (図 20 を参照)。

A User s Delete is Canceled in the Face of a Concurrency Violation

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