適用於: .NET Framework
.NET
.NET 標準
在多使用者環境中,資料庫中的資料更新有兩種模型:樂觀並行控制和悲觀並行控制。 DataSet 物件的設計旨在鼓勵對於長時間執行的作業(例如遠端資料處理以及與資料互動)使用樂觀並行存取。
悲觀式並行控制是指在資料來源將資料列鎖定,以防止其他使用者以影響目前使用者的方式修改資料。 在封閉式模型中,當使用者執行某項作業而造成鎖定時,其他使用者在鎖定擁有人解除鎖定前,都無法執行會與鎖定衝突的動作。 這個模型主要用於資料競爭激烈的環境,因為使用鎖定機制保護資料的成本,低於發生併發衝突時回復交易的成本。
因此,在悲觀式並行控制模型中,更新資料列的使用者會建立鎖定。 使用者尚未完成更新並解除鎖定前,其他人都不能變更這個資料列。 因此,悲觀並行控制最適合在鎖定時間較短的情況下實作,例如以程式方式處理記錄時。 當使用者與資料互動,導致記錄在相對較長的一段時間內遭到鎖定時,悲觀並行控制不是可擴充的選項。
注意
如果您需要在同一作業中更新多個資料列,則建立交易會是比使用悲觀鎖定(Pessimistic Locking)更具擴充性的作法。
相較之下,使用樂觀並行控制的使用者在讀取資料列時,不會將其鎖定。 使用者想要更新資料列時,應用程式必須判斷該資料列自從上次讀取後,是否已由另一位使用者變更。 樂觀並行控制通常用於資料爭用較少的環境。 樂觀並行存取可提升效能,因為不需要鎖定記錄,而鎖定記錄需要額外的伺服器資源。 此外,為了保持記錄鎖定,必須要持續連接至資料庫伺服器。 由於在樂觀並行模型中情況並非如此,因此與伺服器的連線可以在更短的時間內服務更多用戶端。
在樂觀並行控制模型中,如果某位使用者從資料庫讀取某個值之後,另一位使用者在第一位使用者嘗試修改該值之前先將其修改,便視為發生衝突。 若要瞭解伺服器解決並行存取違規的方式,最好先從下列範例的說明開始。
下列表格採用樂觀並行存取的範例。
在下午 1:00 時,User1 從資料庫讀取出資料列,取得下列的值:
CustID 姓氏 名字
101 史密斯鮑勃
| 資料行名稱 | 原始值 | 目前的值 | 資料庫中的值 |
|---|---|---|---|
| CustID | 101 | 101 | 101 |
| 姓氏 | Smith | Smith | Smith |
| 名字 | Bob | Bob | Bob |
在下午 1:01 時,User2 讀取同一資料列。
在下午 1:03 時,User2 將 FirstName 從 "Bob" 變更為 "Robert",並更新資料庫。
| 資料行名稱 | 原始值 | 目前的值 | 資料庫中的值 |
|---|---|---|---|
| CustID | 101 | 101 | 101 |
| 姓氏 | Smith | Smith | Smith |
| 名字 | Bob | Robert | Bob |
更新成功,因為更新時資料庫中的值符合 User2 擁有的原始值。
在下午 1:05 時,User1 將 "Bob" 的名字變更為 "James",並嘗試更新資料列。
| 資料行名稱 | 原始值 | 目前的值 | 資料庫中的值 |
|---|---|---|---|
| CustID | 101 | 101 | 101 |
| 姓氏 | Smith | Smith | Smith |
| 名字 | Bob | James | Robert |
此時,User1 遇到樂觀並行存取違規,因為資料庫中的值 ("Robert") 不再與 User1 原先預期的原始值 ("Bob") 相符。 並行存取衝突只是告知您更新失敗。 現在需要決定是否要以 User1 提供的變更覆寫 User2 提供的變更,或取消 User1 的變更。
測試樂觀並行存取衝突
有數種方法可用來檢測是否發生樂觀並行存取衝突。 其中一種是將時間戳記資料行納入資料表中。
資料庫一般會提供時間戳記功能,可用來辨識記錄上回更新的日期和時間。 採用這項技巧時,資料表定義會包含時間戳記資料行。 只要記錄一更新,時間戳記便會隨之更新以反映目前的日期和時間。
在樂觀並行存取違規的測試中,時間戳記資料行會在任何查詢資料表內容時一併傳回。 嘗試更新時,資料庫中時間戳記的值便會與修改過之資料列中所含的原始時間戳記值比較。 若兩值相符,就會執行更新,並以目前的時間來更新時間戳記資料行的值以反映更新。 如果兩者不相符,便發生了樂觀並行控制衝突。
另一種測試是否發生樂觀並行衝突的方法,是驗證某一資料列中所有原始資料行的值,是否仍與資料庫中的對應值一致。 例如,思考一下下列查詢:
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,則可能必須擴充 WHERE 子句,以檢查區域資料表和資料來源內是否有相符的 Null 參考。 例如,以下 UPDATE 陳述驗證了本地列中的空參考是否仍與資料來源的空參考相符,或本地列的值是否仍與資料來源的值相符。
UPDATE Table1 Set Col1 = @NewVal1
WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1
使用樂觀並行模型時,您也可以選擇套用較寬鬆的條件。 例如,若在 WHERE 子句中只使用主鍵資料行,則無論其他資料行自上次查詢後是否已更新,資料都會被覆寫。 您也可以只在特定資料行套用 WHERE 子句,以覆寫資料 (除非特定欄位在上次查詢後已經更新)。
DataAdapter.RowUpdated 事件
物件的 DataAdapter 事件可搭配前面所述的技術使用,以便在發生樂觀並行存取衝突時向您的應用程式發出通知。 每回嘗試從 DataSet 更新 Modified 資料列時,就會發生 RowUpdated。 如此可讓您加入特殊處理程式碼,包括發生例外狀況時的處理、加入自訂錯誤資訊、加入重試邏輯等等。
RowUpdatedEventArgs 物件會傳回 RecordsAffected 屬性,此屬性包含資料表內已修改過的資料列中,受特定更新命令影響的資料列數目。 將更新命令設為檢查樂觀並行存取後,因此當發生樂觀並行存取違規時,RecordsAffected 屬性會傳回 0,因為沒有任何記錄被更新。 若發生這種情況,就會發生例外狀況。
RowUpdated 事件可讓您設定適當的 RowUpdatedEventArgs.Status 值 (如 UpdateStatus.SkipCurrentRow),以處理這種情況並避免例外狀況的發生。 如需 RowUpdated 事件的詳細資訊,請參閱處理 DataAdapter 事件。
或者,您也可以先將 DataAdapter.ContinueUpdateOnError 設定為 true 後,再呼叫 Update,並於 Update 完成後,對儲存於特定資料列 RowError 屬性中的錯誤資訊做出回應。 如需詳細資訊,請參閱資料列錯誤資訊。
樂觀並行控制範例
以下是一個簡單範例,會將 UpdateCommand 設定於 DataAdapter 中,以測試樂觀並行存取,然後使用 RowUpdated 事件來測試樂觀並行存取衝突。 當發生樂觀並行存取違規時,應用程式會設定發出更新作業所針對之資料列的 RowError,以反映樂觀並行存取違規。
請注意,傳遞至 UPDATE 命令之 WHERE 子句的參數值會對應至各個欄位的 Original 值。
using System;
using System.Data;
using Microsoft.Data.SqlClient;
class Program
{
static void Main(string[] args)
{
string connectionString = "Data Source = localhost; Integrated Security = true; Initial Catalog = Northwind";
using (SqlConnection connection = new SqlConnection(connectionString))
{
// Assumes connection is a valid SqlConnection.
SqlDataAdapter adapter = new SqlDataAdapter(
"SELECT CustomerID, CompanyName FROM Customers ORDER BY CustomerID",
connection);
// The Update command checks for optimistic concurrency violations
// in the WHERE clause.
adapter.UpdateCommand = new SqlCommand("UPDATE Customers Set CustomerID = @CustomerID, CompanyName = @CompanyName " +
"WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName", connection);
adapter.UpdateCommand.Parameters.Add(
"@CustomerID", SqlDbType.NChar, 5, "CustomerID");
adapter.UpdateCommand.Parameters.Add(
"@CompanyName", SqlDbType.NVarChar, 30, "CompanyName");
// Pass the original values to the WHERE clause parameters.
SqlParameter parameter = adapter.UpdateCommand.Parameters.Add(
"@oldCustomerID", SqlDbType.NChar, 5, "CustomerID");
parameter.SourceVersion = DataRowVersion.Original;
parameter = adapter.UpdateCommand.Parameters.Add(
"@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName");
parameter.SourceVersion = DataRowVersion.Original;
// Add the RowUpdated event handler.
adapter.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);
DataSet dataSet = new DataSet();
adapter.Fill(dataSet, "Customers");
// Modify the DataSet contents.
adapter.Update(dataSet, "Customers");
foreach (DataRow dataRow in dataSet.Tables["Customers"].Rows)
{
if (dataRow.HasErrors)
Console.WriteLine(dataRow[0] + "\n" + dataRow.RowError);
}
}
}
protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args)
{
if (args.RecordsAffected == 0)
{
args.Row.RowError = "Optimistic Concurrency Violation Encountered";
args.Status = UpdateStatus.SkipCurrentRow;
}
}
}