樂觀並行
多使用者環境中,更新資料庫的資料時可使用兩種模型:樂觀並行和悲觀並行。DataSet 物件的設計鼓勵在進行資料遠端操作、使用者與資料互動等的長時間活動時,採用樂觀並行。
悲觀並行包括鎖定資料來源的資料行,以免使用者修改資料而影響其他使用者。在悲觀模型中,當使用者執行某項作業造成鎖定時,其他使用者在鎖定擁有人解除鎖定前,都無法執行與鎖定衝突的動作。這個模型主要應用的環境是經常爭用資料,而且鎖定保護資料的成本低於發生並行衝突時復原交易的成本。
所以,悲觀並行模型中,使用者若在讀取資料列時試圖變更內容,即造成鎖定。使用者尚未完成更新並解除鎖定前,其他人都不能變更這個資料列。因此,悲觀並行最適合應用於鎖定時間短的情況,就像以程式設計的方式處理資料錄的情況。但使用者與資料互動時,悲觀並行不可設定大小,所以資料錄會被鎖定達相當長的時間。
相較下,採用樂觀並行的使用者不需要鎖定資料列即可進行讀取。使用者想要更新資料列時,應用程式必須判斷資料列自從上次讀取後,是否已被另一位使用者變更。樂觀並行一般用於不常爭用資料的環境。這個模型由於不需要鎖定資料錄,所以能改善效能,鎖定資料錄則須要額外的伺服器資源;此外,為了保持資料錄鎖定,必須要永續連接至資料庫伺服器。由於樂觀並行模型沒有這種限制,數量龐大的用戶端可用更少的時間連接至伺服器。
樂觀並行模型中,如果使用者甲已收到來自資料庫的值,而這時使用者乙搶在使用者甲之前修改這個值,便會被視為發生違規。
下列表格代表樂觀並行的範例。
在下午 1:00 時,User1 從資料庫讀取出資料列,取得下列的值:
CustID LastName FirstName
101 Smith Bob
資料行名稱 | 原始值 | 目前值 | 資料庫中的值 |
---|---|---|---|
CustID | 101 | 101 | 101 |
LastName | Smith | Smith | Smith |
FirstName | Bob | Bob | Bob |
在下午 1:01 時,User2 讀取同一資料列。
在下午 1:03 時,User2 將 FirstName 從「Bob」變更為「Robert」,並更新資料庫。
資料行名稱 | 原始值 | 目前值 | 資料庫中的值 |
---|---|---|---|
CustID | 101 | 101 | 101 |
LastName | Smith | Smith | Smith |
FirstName | Bob | Robert | Bob |
更新成功,因為更新時資料庫中的值符合 User2 擁有的原始值。
在下午 1:05 時,User1 將 Bob 的姓變更為「James」,並嘗試更新資料列。
資料行名稱 | 原始值 | 目前值 | 資料庫中的值 |
---|---|---|---|
CustID | 101 | 101 | 101 |
LastName | Smith | Smith | Smith |
FirstName | Bob | James | Robert |
此時,User1 發生了樂觀並行違規的情況,因為資料庫中的值不再符合 User1 原先預期的原始值。現在必須決定要用 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 陳述式驗證區域資料列中的 Null 參考仍然與資料來源的 Null 參考相符,或是區域資料列的值仍然與資料來源的值相符。
UPDATE Table1 Set Col1 = @NewVal1
WHERE (@OldVal1 IS NULL AND Col1 IS NULL) OR Col1 = @OldVal1
使用樂觀並行模型時,您也可以選擇套用較寬鬆的準則。例如,在 WHERE 子句中僅使用主索引鍵資料行時,不論另一個資料行是否在上次查詢後更新,都會造成資料被覆寫。您也可以只在特定資料行套用 WHERE 子句,使資料被覆寫 (除非特定欄位在上次查詢後已經更新)。
DataAdapter.RowUpdated 事件
DataAdapter.RowUpdated 事件可配合上述技巧使用,於樂觀並行違規時向您的應用程式提供告知。每回嘗試從 DataSet 更新 Modified 資料列時,就會發生 RowUpdated。如此可讓您加入特殊處理程式碼,包括發生例外狀況 (Exception) 時的處理、加入自訂錯誤資訊、加入重試邏輯等等。RowUpdatedEventArgs 物件傳回 RecordsAffected 屬性,以及資料表內修改過的資料列中,被特定更新命令影響的資料列數目。設定更新命令以測試樂觀並行後,發生樂觀並行違規時,由於沒有任何資料錄會被更新,所以 RecordsAffected 屬性的傳回值為 0。若發生這種情況,即發生例外狀況。您可以使用 RowUpdated 事件來設定適當的 RowUpdatedEventArgs.Status 值 (例如 UpdateStatus.SkipCurrentRow) 來處理這個事件,並避免發生例外狀況。如需 RowUpdated 事件的詳細資訊,請參閱使用 DataAdapter 事件。
或者,您也可選擇將 DataAdapter.ContinueUpdateOnError 設定為 true,再呼叫 Update,並於 Update 完成後,對特定資料列內 RowError 屬性儲存的錯誤訊息做出回應。如需詳細資訊,請參閱加入和讀取資料列錯誤資訊。
樂觀並行範例
下列簡單範例將設定 DataAdapter 的 UpdateCommand 以測試樂觀並行,並使用 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 資料提供者存取資料