逐步解說:擴充本機資料庫快取以支援雙向同步處理
您可以在專案中加入 [本機資料庫快取] 項目,設定本機 SQL Server Compact 3.5 資料庫快取,以及產生一組啟用 Microsoft Synchronization Services for ADO.NET 的部分類別。 因為 Visual Studio 會產生部分類別,您可以撰寫程式碼以加入同步處理功能,同時仍然能夠檢視與變更 [設定資料同步處理] 對話方塊中的設定。 如需部分類別的詳細資訊,請參閱 HOW TO:將類別分割成部分類別 (類別設計工具)。
根據預設,[設定資料同步處理] 對話方塊僅能讓您針對下載作業設定 Synchronization Services。 這表示在您設定資料同步處理後,呼叫 Synchronize() 時只會將變更由伺服器下載至用戶端資料庫。 擴充同步處理程式碼最常見的方法之一,是設定雙向 (Bidirectional) 同步處理。 如此一來,您就能由用戶端將變更上載至伺服器。 若要啟用雙向同步處理,我們建議您以下列方式擴充產生的程式碼:
將同步處理方向設定為雙向。
加入程式碼以處理同步處理衝突。
由同步處理命令移除伺服器追蹤資料行。
必要條件
在開始這個逐步解說前,您必須先完成逐步解說:建立偶爾連接的應用程式。 完成上述的逐步解說後,您會有包含 [本機資料庫快取] 項目與 Windows Form 應用程式的專案,即能由 Northwind Customers 資料表將變更下載至 SQL Server Compact 資料庫。 現在您已準備就緒,可以載入此逐步解說方案並加入雙向功能。
注意事項 |
---|
您的電腦可能會在下列說明中,以不同名稱或位置顯示某些 Visual Studio 使用者介面項目。 您所擁有的 Visual Studio 版本以及使用的設定會決定這些項目。 如需詳細資訊,請參閱 Visual Studio 設定。 |
若要開啟 OCSWalkthrough 方案
開啟 Visual Studio。
在 [檔案] 功能表上開啟現有的方案或專案,並找出 OCSWalkthrough 方案, 也就是 OCSWalkthrough.sln 檔案。
設定同步處理方向
[設定資料同步處理] 對話方塊能讓您將 SyncDirection() 屬性設定為 DownloadOnly() 或 Snapshot()。 若要啟用雙向同步處理,則針對要啟用將變更上載的功能的每個資料表,將 SyncDirection() 屬性設定為 Bidirectional()。
若要設定同步處理方向
以滑鼠右鍵按一下 [NorthwindCache.sync],然後按一下 [檢視程式碼。 您第一次做這個動作時,Visual Studio 會在 [方案總管] 中的 [NorthwindCache.sync] 節點下建立一個 NorthwindCache 檔案。 這個檔案中包含了一個 NorthwindCacheSyncAgent 部分類別,您可以視需要加入其他的類別。
在 NorthwindCache 類別檔案中加入程式碼,讓 NorthwindCacheSyncAgent.OnInitialized() 方法類似下列程式碼:
partial void OnInitialized() { this.Customers.SyncDirection = Microsoft.Synchronization.Data.SyncDirection.Bidirectional; }
Private Sub OnInitialized() Me.Customers.SyncDirection = Microsoft.Synchronization.Data.SyncDirection.Bidirectional End Sub
在 [程式碼編輯器] 中開啟 Form1。
在 Form1 檔案中,修改在 SynchronizeButton_Click 事件處理常式中的程式碼行,以包含上載和下載統計資料:
MessageBox.Show("Changes downloaded: " + syncStats.TotalChangesDownloaded.ToString() + Environment.NewLine + "Changes uploaded: " + syncStats.TotalChangesUploaded.ToString());
MessageBox.Show("Changes downloaded: " & _ syncStats.TotalChangesDownloaded.ToString & _ Environment.NewLine & _ "Changes uploaded: " & _ syncStats.TotalChangesUploaded.ToString)
測試應用程式
現在應用程式已設定為會在同步處理期間一併執行上載與下載。
若要測試應用程式
按 F5。
更新表單中的一項記錄,然後按一下 [儲存] 按鈕 (工具列上的磁碟圖示)。
按一下 [開始同步處理]。
隨即會出現一個訊息方塊,內含有關同步處理記錄的資訊。 這些統計資料會顯示有一列已上載,也下載一列,雖然在伺服器上沒有變更。 發生此額外下載的原因,是因為來自用戶端的變更在套用至伺服器後,會回應 (Echo) 至用戶端。 如需詳細資訊,請參閱 HOW TO:使用自訂變更追蹤系統中的<判斷變更資料的用戶端>一節 (英文)。
按一下 [確定] 關閉訊息方塊,但讓應用程式繼續執行。
現在您將在用戶端和伺服器上同時變更同樣的記錄,在同步處理期間強迫產生衝突 (並行違規)。
若要測試應用程式並強迫產生衝突
更新表單中的一項記錄,然後按一下 [儲存] 按鈕。
在應用程式執行中,使用 [伺服器總管]/[資料庫總管] (或其他資料庫管理工具) 連接至伺服器資料庫。
為示範衝突解決方式的預設行為,在 [伺服器總管]/[資料庫總管] 中,對您在表單中所更新的同一筆記錄進行更新,但是變更為不同的值,然後認可變更 (離開修改過的資料列即可)。
返回表單並按一下 [開始同步處理]。
驗證在應用程式方格中與伺服器資料庫中的更新資料。 請注意,您在伺服器上所做的更新,已覆寫用戶端上的更新。 如需如何變更此衝突解決方法之行為的資訊,請參閱本主題的下一節「加入程式碼以處理同步處理衝突」。
加入程式碼以處理同步處理衝突
在 Synchronization Services 中,如果有一個資料列在同步處理的間隔中,同時在用戶端與伺服器上受到變更,就會出現衝突。 Synchronization Services 提供一組功能,可用來偵測及解決衝突。 在本節中,您將加入基本的處理方式,解決同一個資料列同時在用戶端與伺服器上更新所造成的衝突。 其他類型的衝突包括了同一個資料列在一個資料庫中被刪除,但在另一個資料庫中卻已更新,或有主索引鍵重複的資料列同時插入兩個資料庫的情況。 如需偵測與解決衝突之方法的詳細資訊,請參閱 HOW TO:處理資料衝突與錯誤 (英文)。
注意事項 |
---|
範例程式碼提供了衝突處理的基本範例。 您處理衝突的方式要根據您應用程式和商務邏輯的需求而定。 |
加入程式碼以處理伺服器 ApplyChangeFailed 事件與用戶端 ApplyChangeFailed 事件。 當資料列由於衝突或錯誤而無法套用時,就會引發這些事件。 處理這些事件的方法會檢查衝突類型,並指定將用戶端變更強制寫入伺服器資料庫,以解決用戶端更新 / 伺服器更新衝突。 將更新套用至伺服器資料庫的同步處理命令,含有辨識何時應強制執行變更的邏輯。 這項命令會包含在本主題下一節「由同步處理命令移除伺服器追蹤資料行」的程式碼中。
您為加入衝突處理所執行的步驟,會根據您使用的是 C# 或 Visual Basic 而有所不同。
若要加入衝突處理
如果您使用 C#,請將下列程式碼加入至 NorthwindCache.cs 和 Form1.cs 中。 在 NorthwindCache.cs 中,將下列程式碼加到 NorthwindCacheSyncAgent 類別的結尾之後:
public partial class NorthwindCacheServerSyncProvider { partial void OnInitialized() { this.ApplyChangeFailed += new System.EventHandler<Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs> (NorthwindCacheServerSyncProvider_ApplyChangeFailed); } private void NorthwindCacheServerSyncProvider_ApplyChangeFailed(object sender, Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs e) { if (e.Conflict.ConflictType == Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate) { // Resolve a client update / server update conflict by force writing // the client change to the server database. System.Windows.Forms.MessageBox.Show("A client update / server update conflict " + "was detected at the server."); e.Action = Microsoft.Synchronization.Data.ApplyAction.RetryWithForceWrite; } } } public partial class NorthwindCacheClientSyncProvider { public void AddHandlers() { this.ApplyChangeFailed += new System.EventHandler<Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs> (NorthwindCacheClientSyncProvider_ApplyChangeFailed); } private void NorthwindCacheClientSyncProvider_ApplyChangeFailed(object sender, Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs e) { if (e.Conflict.ConflictType == Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate) { // Resolve a client update / server update conflict by keeping the // client change. e.Action = Microsoft.Synchronization.Data.ApplyAction.Continue; } } }
在 Form1.cs 中修改 SynchronizeButton_Click 事件處理常式中的程式碼,以呼叫您在前一個步驟中加入至 NorthwindCache.cs 的 AddHandler 方法:
NorthwindCacheSyncAgent syncAgent = new NorthwindCacheSyncAgent(); NorthwindCacheClientSyncProvider clientSyncProvider = (NorthwindCacheClientSyncProvider)syncAgent.LocalProvider; clientSyncProvider.AddHandlers(); Microsoft.Synchronization.Data.SyncStatistics syncStats = syncAgent.Synchronize();
如果您使用 Visual Basic,則將下列程式碼加到 NorthwindCache.vb 中 NorthwindCacheSyncAgent 類別的 End Class 陳述式之後。
Partial Public Class NorthwindCacheServerSyncProvider Private Sub NorthwindCacheServerSyncProvider_ApplyChangeFailed( _ ByVal sender As Object, ByVal e As _ Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs) _ Handles Me.ApplyChangeFailed If e.Conflict.ConflictType = _ Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate Then ' Resolve a client update / server update conflict by force writing ' the client change to the server database. MessageBox.Show("A client update / server update" & _ " conflict was detected at the server.") e.Action = Microsoft.Synchronization.Data.ApplyAction.RetryWithForceWrite End If End Sub End Class Partial Public Class NorthwindCacheClientSyncProvider Private Sub NorthwindCacheClientSyncProvider_ApplyChangeFailed( _ ByVal sender As Object, ByVal e As _ Microsoft.Synchronization.Data.ApplyChangeFailedEventArgs) _ Handles Me.ApplyChangeFailed If e.Conflict.ConflictType = _ Microsoft.Synchronization.Data.ConflictType.ClientUpdateServerUpdate Then ' Resolve a client update / server update conflict by keeping the ' client change. e.Action = Microsoft.Synchronization.Data.ApplyAction.Continue End If End Sub End Class
若要同步處理並檢視衝突解決方法
按 F5。
更新表單中的一項記錄,然後按一下 [儲存] 按鈕。
在 [伺服器總管]/[資料庫總管] 中,對您在表單中所更新的同一筆記錄進行更新,但是變更為不同的值,然後認可變更。
返回表單並按一下 [開始同步處理]。
驗證在應用程式方格中與伺服器資料庫中的更新資料。 請注意,您在用戶端上所做的更新,已覆寫伺服器上的更新。
由同步處理命令移除伺服器追蹤資料行
建立 [本機資料庫快取] 時,用於追蹤伺服器資料庫中之變更的資料行會下載至用戶端 (在本逐步解說中,這些資料行指的是 CreationDate 和 LastEditDate)。若要支援雙向同步處理,並協助確保聚合用戶端與伺服器上的資料,請由套用變更至伺服器資料庫的 SQL 命令移除這些資料行。 您也可以由選取伺服器上的變更以套用至用戶端的命令中,將這些資料行移除,但這不是必要的。 由於用戶端資料庫中對某些結構描述變更的限制,因此無法刪除這些資料行。 如需同步處理命令的詳細資訊,請參閱 HOW TO:指定快照、下載、上載與雙向同步處理 (英文)。
注意事項 |
---|
如果您使用 SQL Server 2008 變更追蹤,追蹤資料行將不會加到您的資料表中。 在這樣的情況下,您不需要改變將變更套用至伺服器的命令。 |
下列程式碼會重新定義兩個設定為 Customers 資料表之 SyncAdapter 物件上的屬性,分別是 InsertCommand() 和 UpdateCommand() 屬性。 這些由 [設定資料同步處理] 對話方塊產生的命令,包含對 CreationDate 和 LastEditDate 資料行的參考。 在下列程式碼中,這些命令會在CustomersSyncAdapter 類別的 OnInitialized 方法中重新定義。 因為對 CreationDate 和 LastEditDate 資料行沒有影響,因此 DeleteCommand() 屬性未重新定義。
每個 SQL 命令中的變數都會用於在 Synchronization Services、用戶端和伺服器間傳遞資料和中繼資料 (Metadata)。 下列工作階段變數會用於下方的命令:
@sync\_row\_count:傳回受到伺服器上前一次執行的作業所影響的資料列數。 在 SQL Server 資料庫中,@@rowcount 會提供此變數的值。
@sync\_force\_write:用於強制套用因為衝突或錯誤而套用失敗的變更。
@sync\_last\_received\_anchor:用於定義在工作階段中要同步處理的變更集。
如需工作階段變數的詳細資訊,請參閱 HOW TO:使用工作階段變數 (英文)。
若要由同步處理命令移除追蹤資料行
在 NorthwindCache 類別 (NorthwindCache.vb 或 NorthwindCache.cs) 中,將下列程式碼加到 NorthwindCacheServerSyncProvider 類別的 End Class 陳述式之後。
public partial class CustomersSyncAdapter { partial void OnInitialized() { // Redefine the insert command so that it does not insert values // into the CreationDate and LastEditDate columns. System.Data.SqlClient.SqlCommand insertCommand = new System.Data.SqlClient.SqlCommand(); insertCommand.CommandText = "INSERT INTO dbo.Customers ([CustomerID], [CompanyName], " + "[ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], " + "[Country], [Phone], [Fax] )" + "VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, " + "@Region, @PostalCode, @Country, @Phone, @Fax) SET @sync_row_count = @@rowcount"; insertCommand.CommandType = System.Data.CommandType.Text; insertCommand.Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar); insertCommand.Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@Address", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@City", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@Region", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@Country", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar); insertCommand.Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int); insertCommand.Parameters["@sync_row_count"].Direction = System.Data.ParameterDirection.Output; this.InsertCommand = insertCommand; // Redefine the update command so that it does not update values // in the CreationDate and LastEditDate columns. System.Data.SqlClient.SqlCommand updateCommand = new System.Data.SqlClient.SqlCommand(); updateCommand.CommandText = "UPDATE dbo.Customers SET [CompanyName] = @CompanyName, [ContactName] " + "= @ContactName, [ContactTitle] = @ContactTitle, [Address] = @Address, [City] " + "= @City, [Region] = @Region, [PostalCode] = @PostalCode, [Country] = @Country, " + "[Phone] = @Phone, [Fax] = @Fax " + "WHERE ([CustomerID] = @CustomerID) AND (@sync_force_write = 1 " + "OR ([LastEditDate] <= @sync_last_received_anchor)) SET @sync_row_count = @@rowcount"; updateCommand.CommandType = System.Data.CommandType.Text; updateCommand.Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@Address", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@City", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@Region", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@Country", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar); updateCommand.Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar); updateCommand.Parameters.Add("@sync_force_write", System.Data.SqlDbType.Bit); updateCommand.Parameters.Add("@sync_last_received_anchor", System.Data.SqlDbType.DateTime); updateCommand.Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int); updateCommand.Parameters["@sync_row_count"].Direction = System.Data.ParameterDirection.Output; this.UpdateCommand = updateCommand; } }
Partial Public Class CustomersSyncAdapter Private Sub OnInitialized() ' Redefine the insert command so that it does not insert values ' into the CreationDate and LastEditDate columns. Dim insertCommand As New System.Data.SqlClient.SqlCommand With insertCommand .CommandText = "INSERT INTO dbo.Customers ([CustomerID], [CompanyName], " & _ "[ContactName], [ContactTitle], [Address], [City], [Region], [PostalCode], " & _ "[Country], [Phone], [Fax] )" & _ "VALUES (@CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, " & _ "@Region, @PostalCode, @Country, @Phone, @Fax) SET @sync_row_count = @@rowcount" .CommandType = System.Data.CommandType.Text .Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar) .Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar) .Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar) .Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Address", System.Data.SqlDbType.NVarChar) .Parameters.Add("@City", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Region", System.Data.SqlDbType.NVarChar) .Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Country", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar) .Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int) .Parameters("@sync_row_count").Direction = ParameterDirection.Output End With Me.InsertCommand = insertCommand ' Redefine the update command so that it does not update values ' in the CreationDate and LastEditDate columns. Dim updateCommand As New System.Data.SqlClient.SqlCommand With updateCommand .CommandText = "UPDATE dbo.Customers SET [CompanyName] = @CompanyName, [ContactName] " & _ "= @ContactName, [ContactTitle] = @ContactTitle, [Address] = @Address, [City] " & _ "= @City, [Region] = @Region, [PostalCode] = @PostalCode, [Country] = @Country, " & _ "[Phone] = @Phone, [Fax] = @Fax " & _ "WHERE ([CustomerID] = @CustomerID) AND (@sync_force_write = 1 " & _ "OR ([LastEditDate] <= @sync_last_received_anchor)) SET @sync_row_count = @@rowcount" .CommandType = System.Data.CommandType.Text .Parameters.Add("@CompanyName", System.Data.SqlDbType.NVarChar) .Parameters.Add("@ContactName", System.Data.SqlDbType.NVarChar) .Parameters.Add("@ContactTitle", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Address", System.Data.SqlDbType.NVarChar) .Parameters.Add("@City", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Region", System.Data.SqlDbType.NVarChar) .Parameters.Add("@PostalCode", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Country", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Phone", System.Data.SqlDbType.NVarChar) .Parameters.Add("@Fax", System.Data.SqlDbType.NVarChar) .Parameters.Add("@CustomerID", System.Data.SqlDbType.NChar) .Parameters.Add("@sync_force_write", System.Data.SqlDbType.Bit) .Parameters.Add("@sync_last_received_anchor", System.Data.SqlDbType.DateTime) .Parameters.Add("@sync_row_count", System.Data.SqlDbType.Int) .Parameters("@sync_row_count").Direction = ParameterDirection.Output End With Me.UpdateCommand = updateCommand End Sub End Class
測試應用程式
若要同步處理並檢視追蹤資料行更新
按 F5。
變更 LastEditDate 資料行中的值,然後按一下 [儲存] 按鈕,更新表單中的一項記錄。
返回至表單並按一下 [開始同步處理]。
驗證在應用程式方格中與伺服器資料庫中的更新資料。 請注意,伺服器上資料行中的值,已覆寫用戶端上的更新。 更新程序如下所示:
Synchronization Services 會判斷用戶端上有一個資料列受到變更。
在同步處理期間會上載這個資料列,並套用至伺服器資料庫中的資料表。 然而,追蹤資料行並不包含在更新陳述式中。 Synchronization Services 實際上對資料表執行的是「假更新」。
資料列接著會回應至用戶端,但由伺服器選取變更的命令有包含追蹤資料行。 因此,在用戶端所做的變更會由伺服器上的值覆寫。
後續步驟
在本逐步解說中,您以基本衝突處理設定了雙向同步處理,並處理了在用戶端資料庫中使用伺服器追蹤資料行的可能問題。 藉著使用部分類別,您還可以在其他重要的面向上擴充 [本機資料庫快取] 程式碼。 例如,您可以重新定義由伺服器資料庫選取變更的 SQL 命令,讓資料在下載至用戶端時先篩選。 我們建議您閱讀本文件中的 HOW TO 主題,了解您可以加入或變更同步處理程式碼的方式,以符合您應用程式的需求。 如需詳細資訊,請參閱如何設計一般用戶端和伺服器同步處理工作的程式 (英文)。