次の方法で共有


オプティミスティック同時実行制御

マルチユーザー環境には、データベースのデータ更新のための 2 つのモデルがあります。オプティミスティック同時実行制御とペシミスティック同時実行制御です。DataSet オブジェクトは、データをリモート処理するときやユーザーがデータと対話するときのように長時間にわたる利用状況では、オプティミスティック同時実行制御の使用を促進するように設計されています。

ペシミスティック同時実行制御では、ユーザーが他のユーザーに影響を与えるようなデータ変更を行うのを防止するために、データ ソースで行をロックする必要があります。ペシミスティック モデルでは、あるユーザーがロックの適用される操作を実行すると、他のユーザーは、ロックが解除されるまで、競合する操作を実行できません。このモデルは主に、データの競合が多く、同時実行の競合が発生する場合、データをロックで保護するコストがトランザクションをロールバックするコストを下回るときに使用されます。

したがって、ペシミスティック同時実行制御モデルでは、行を変更するために読み込むユーザーは、ロックを設定します。更新を終了してロックを解除するまでは、他のユーザーはその行を変更できません。そのため、ペシミスティック同時実行制御は、レコードをプログラムで処理する場合のようにロック時間が短いときの実装に適しています。ペシミスティック同時実行制御は、ユーザーがデータと対話しているためにレコードが比較的長時間ロックされる場合には適していません。

これに対して、オプティミスティック同時実行制御を使用するユーザーは、行を読み取るときに行をロックしません。ユーザーが行を更新するときは、行の読み取り後に別のユーザーがその行を変更したかどうかをアプリケーションで確認する必要があります。オプティミスティック同時実行制御は、一般に、データの競合が少ない環境で使用されます。オプティミスティック同時実行制御を使用すると、レコードをロックする必要がないため、パフォーマンスが上がります。レコードをロックするには、追加のサーバー リソースが必要です。また、レコードのロックを管理するためにデータベース サーバーに永続的に接続している必要があります。これに対して、オプティミスティック同時実行制御モデルでは、サーバーとの接続が固定していないため、より短い時間で多数のクライアントを扱うことができます。

オプティミスティック同時実行制御モデルでは、ユーザーがデータベースから値を受け取った後、その値を変更する前に別のユーザーがその値を変更した場合に、違反が発生したと見なされます。

オプティミスティック同時実行制御の例を次の表に示します。

午後 1 時に、User1 が、次の値を持つデータベースから行を読み取ります。

CustID     LastName     FirstName

101          Smith             Bob

列名 元の値 現在の値 データベース内の値
CustID 101 101 101
LastName Smith Smith Smith
FirstName Bob Bob Bob

午後 1 時 1 分に、User2 が同じ行を読み取ります。

午後 1 時 3 分に、User2 が FirstName を "Bob" から "Robert" に変更し、データベースを更新します。

列名 元の値 現在の値 データベース内の値
CustID 101 101 101
LastName Smith Smith Smith
FirstName Bob Robert Bob

更新時のデータベース内の値は User2 が持つ元の値と一致しているため、更新は成功します。

午後 1 時 5 分に、User1 が Bob の名を "James" に変更し、行の更新を試みます。

列名 元の値 現在の値 データベース内の値
CustID 101 101 101
LastName Smith Smith Smith
FirstName Bob James Robert

この時点では、データベース内の値は User1 が期待していた元の値と一致していないため、User1 による更新はオプティミスティック同時実行制御違反となります。ここで、User2 による変更を User1 による変更で上書きするか、または User1 による変更をキャンセルするかの決定が必要になります。

オプティミスティック同時実行制御違反テスト

オプティミスティック同時実行制御違反をテストするには、いくつか方法があります。1 つは、テーブルにタイムスタンプ列を含める方法です。一般に、データベースには、レコードを最後に更新した日付と時刻を識別するために使用するタイムスタンプ機能が用意されています。この機能を使用すると、timestamp 列がテーブル定義に組み込まれます。レコードが更新されると、タイムスタンプが更新されて現在の日付と時刻が反映されます。オプティミスティック同時実行制御違反テストでは、テーブル内容についてのクエリによって timestamp 列が返されます。更新しようとすると、データベースのタイムスタンプ値と、変更行に格納されている元のタイムスタンプが比較されます。一致した場合、更新が実行され、timestamp 列が現在の時刻に更新されてその更新が反映されます。一致しなかった場合は、オプティミスティック同時実行制御違反が発生します。

オプティミスティック同時実行制御違反をテストするためのもう 1 つの方法は、行の元のすべての列の値が、データベース内の値とまだ一致しているかどうかをチェックすることです。たとえば、次のクエリを見てみましょう。

SELECT Col1, Col2, Col3 FROM Table1

Table1 内の行の更新時にオプティミスティック同時実行制御違反についてテストするために、次の UPDATE ステートメントを実行します。

UPDATE Table1 Set Col1 = @NewCol1Value,
              Set Col2 = @NewCol2Value,
              Set Col3 = @NewCol3Value
WHERE Col1 = @OldCol1Value AND
      Col2 = @OldCol2Value AND
      Col3 = @OldCol3Value

元の値がデータベース内の値と一致する間は、更新が実行されます。値が変更された場合は、WHERE 句で一致する値を見つけることができないため、その行は更新できません。

クエリで、常に固有の主キー値を返すことをお勧めします。固有の主キーを返さないと、UPDATE ステートメントによって複数の行が更新されることがあり、意図に反した結果となります。

データ ソースの列で null が使用できる場合は、ローカル テーブルおよびデータ ソースで一致する null 参照がないかどうかをチェックするように WHERE 句を拡張する必要があります。たとえば、次の UPDATE ステートメントは、ローカル行の null 参照がデータ ソースの null 参照とまだ一致しているかどうか、つまり、ローカル行の値がデータ ソースの値とまだ一致しているかどうかをチェックします。

UPDATE Table1 Set Col1 = @NewVal1
  WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1

オプティミスティック同時実行制御モデルを使用するときは、より制限の少ない抽出条件を適用するように選択することもできます。たとえば、WHERE 句で主キー列だけを使用すると、前回のクエリ後にそれ以外の列が更新されたかどうかに関係なく、データが上書きされます。WHERE 句を特定の列だけに適用することもできます。特定の列だけに WHERE 句を適用した場合は、特定のフィールドが前回のクエリ後に更新されていない限りデータが上書きされます。

DataAdapter.RowUpdated イベント

これまでに説明した方法に関連して DataAdapter.RowUpdated イベントを使用すると、オプティミスティック同時実行制御違反をアプリケーションに通知できます。RowUpdated は、DataSet から Modified 行の更新を行ったときに発生します。これにより、例外発生時の処理、カスタム エラー情報の追加、再試行ロジックの追加などの特別の処理コードを追加できます。RowUpdatedEventArgs オブジェクトは、テーブルの変更行に対する特定の更新コマンドによって影響された行数が設定された RecordsAffected プロパティを返します。オプティミスティック同時実行制御をテストするように更新コマンドを設定した場合は、オプティミスティック同時実行制御違反の発生時に、RecordsAffected プロパティは値 0 を結果として返します。値が 0 なのはレコードが更新されないためです。この場合、例外がスローされます。RowUpdated イベントを使用すると、発生したイベントを処理したり、UpdateStatus.SkipCurrentRow のような RowUpdatedEventArgs.Status 値を設定することで例外を抑止したりできます。RowUpdated イベントの詳細については、「DataAdapter イベントの使用」を参照してください。

オプションで、Update を呼び出す前に DataAdapter.ContinueUpdateOnErrortrue に設定し、Update 完了時に特定の行の RowError プロパティに格納されたエラー情報に応答できます。詳細については、「行のエラー情報の追加と読み取り」を参照してください。

オプティミスティック同時実行制御の例

DataAdapterUpdateCommand をオプティミスティック同時実行制御をテストするように設定してから、RowUpdated イベントを使用してオプティミスティック同時実行制御違反がないかどうかをテストする簡単な例を次に示します。オプティミスティック同時実行制御違反が検出されると、アプリケーションは、オプティミスティック同時実行制御違反が反映されるように更新が実行された行の RowError を設定します。

UPDATE コマンドの WHERE 句に渡されたパラメータ値は、それぞれの列の Original 値に割り当てられることに注意してください。

  Dim nwindConn As SqlConnection = New SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind")

  Dim custDA As SqlDataAdapter = New SqlDataAdapter("SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", nwindConn)

  ' The Update command checks for optimistic concurrency violations in the WHERE clause.
  custDA.UpdateCommand = New SqlCommand("UPDATE Customers (CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " & _
                                        "WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", nwindConn)
  custDA.UpdateCommand.Parameters.Add("@CustomerID", SqlDbType.NChar, 5, "CustomerID")
  custDA.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName")

  ' Pass the original values to the WHERE clause parameters.
  Dim myParm As SqlParameter
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCustomerID", SqlDbType.NChar, 5, "CustomerID")
  myParm.SourceVersion = DataRowVersion.Original
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName")
  myParm.SourceVersion = DataRowVersion.Original

  ' Add the RowUpdated event handler.
  AddHandler custDA.RowUpdated, New SqlRowUpdatedEventHandler(AddressOf OnRowUpdated)

  Dim custDS As DataSet = New DataSet()
  custDA.Fill(custDS, "Customers")

  ' Modify the DataSet contents.

  custDA.Update(custDS, "Customers")

  Dim myRow As DataRow

  For Each myRow In custDS.Tables("Customers").Rows
    If myRow.HasErrors Then Console.WriteLine(myRow(0) & vbCrLf & myRow.RowError)
  Next


Private Shared Sub OnRowUpdated(sender As object, args As SqlRowUpdatedEventArgs)
  If args.RecordsAffected = 0
    args.Row.RowError = "Optimistic Concurrency Violation Encountered"
    args.Status = UpdateStatus.SkipCurrentRow
  End If
End Sub
[C#]
  SqlConnection nwindConn = new SqlConnection("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind");

  SqlDataAdapter custDA = new SqlDataAdapter("SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID", nwindConn);

  // The Update command checks for optimistic concurrency violations in the WHERE clause.
  custDA.UpdateCommand = new SqlCommand("UPDATE Customers (CustomerID, CompanyName) VALUES(@CustomerID, @CompanyName) " +
                                        "WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", nwindConn);
  custDA.UpdateCommand.Parameters.Add("@CustomerID", SqlDbType.NChar, 5, "CustomerID");
  custDA.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName");

  // Pass the original values to the WHERE clause parameters.
  SqlParameter myParm;
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCustomerID", SqlDbType.NChar, 5, "CustomerID");
  myParm.SourceVersion = DataRowVersion.Original;
  myParm = custDA.UpdateCommand.Parameters.Add("@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName");
  myParm.SourceVersion = DataRowVersion.Original;

  // Add the RowUpdated event handler.
  custDA.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);

  DataSet custDS = new DataSet();
  custDA.Fill(custDS, "Customers");

  // Modify the DataSet contents.

  custDA.Update(custDS, "Customers");

  foreach (DataRow myRow in custDS.Tables["Customers"].Rows)
  {
    if (myRow.HasErrors)
      Console.WriteLine(myRow[0] + "\n" + myRow.RowError);
  }


protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args)
{
  if (args.RecordsAffected == 0) 
  {
    args.Row.RowError = "Optimistic Concurrency Violation Encountered";
    args.Status = UpdateStatus.SkipCurrentRow;
  }
}

参照

サンプル ADO.NET シナリオ | DataAdapter および DataSet によるデータベースの更新 | DataAdapter イベントの使用 | 行のエラー情報の追加と読み取り | ADO.NET を使用したデータのアクセス | .NET Framework データ プロバイダによるデータのアクセス