演练:扩展本地数据库缓存以支持双向同步
您可以将**“本地数据库缓存”项添加到项目,以便配置本地 SQL Server Compact 3.5 数据库缓存,并生成一组启用 Microsoft Synchronization Services for ADO.NET 的分部类。 由于 Visual Studio 可生成分部类,因此可以编写一些代码来添加同步功能,同时仍然保留在“配置数据同步”**对话框中查看和更改设置的功能。 有关分部类的更多信息,请参见如何:将类拆分为分部类(类设计器)。
默认情况下,可以使用**“配置数据同步”**对话框将 Synchronization Services 配置为仅用于下载。 这意味着,在配置数据同步之后,调用 Synchronize() 会只将更改从服务器下载到客户端数据库。 扩展同步代码的最常用方式之一是配置双向同步。 这样,可以将更改从客户端上载到服务器。 若要实现双向同步,建议通过以下方式扩展生成的代码:
将同步方向设置为双向。
添加用于处理同步冲突的代码。
从同步命令中移除服务器跟踪列。
系统必备
在开始本演练之前,您必须完成演练:创建偶尔连接的应用程序。 在完成本演练之后,您会拥有一个项目,该项目包含一个**“本地数据库缓存”**项和一个 Windows 窗体应用程序,通过该应用程序,可以将更改从 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。
在窗体中,更新一个记录,然后单击**“保存”**按钮(工具栏上的磁盘图标)。
单击**“立即同步”**。
此时,将显示一个包含已同步记录的信息的消息框。 统计信息显示上载了一行并下载了一行,即使服务器上未进行任何更改也是如此。 由于客户端的更改在应用于服务器之后会回送到客户端,因此会进行另一次下载。 有关更多信息,请参见 How to: Use a Custom Change Tracking System(如何:使用自定义更改跟踪系统)中的“Determining Which Client Made a Data Change”(确定更改了数据的客户端)。
单击**“确定”**关闭消息框,但保持应用程序处于运行状态。
现在,将在客户端和服务器更改同一记录,从而在同步过程中强制发生冲突(并发冲突)。
测试应用程序,强制发生冲突
在窗体中,更新一个记录,然后单击**“保存”**按钮。
在应用程序仍在运行时,使用**“服务器资源管理器”/“数据库资源管理器”**(或其他数据库管理工具)连接到服务器数据库。
若要演示冲突解决方法的默认行为,请在**“服务器资源管理器”/“数据库资源管理器”**中,更新在窗体上更新的记录,但将该记录更改为另一个值,然后提交更改。 (从已修改的行移开。)
返回到窗体,然后单击**“立即同步”**。
验证应用程序网格和服务器数据库中的更新。 请注意,在服务器上进行的更新已覆盖在客户端上进行的更新。 有关如何更改此冲突解决方法行为的信息,请参见本主题的下一节“添加用于处理同步冲突的代码”。
添加用于处理同步冲突的代码
在 Synchronization Services 中,如果在同步之间,某个行同时在客户端和服务器上进行了更改,则该行处于冲突状态。 Synchronization Services 提供了一组可用于检测和解决冲突的功能。 在本节中,将针对在客户端和服务器上更新相同行这种冲突,添加基本处理。 其他种类的冲突包括,在一个数据库中删除了某行而在另一个数据库中更新了该行,或者插入到两个数据库中的行具有重复的主键。 有关检测和解决冲突的更多信息,请参见 How to: Handle Data Conflicts and Errors(如何:处理数据冲突和错误)。
提示
该代码示例提供冲突处理的基本示例。 处理冲突的方式取决于应用程序和业务逻辑的要求。
添加用于处理服务器 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: Specify Snapshot, Download, Upload, and Bidirectional Synchronization(如何:指定快照、下载、上载和双向同步)。
提示
如果使用 SQL Server 2008 更改跟踪,则不会将跟踪列添加到表中。 在这种情况下,不必更改将更改应用于服务器的命令。
下面的代码重新定义两个命令,这两个命令设置为 Customers 表的 SyncAdapter 对象的属性:InsertCommand() 和 UpdateCommand() 属性。 通过**“配置数据同步”**对话框生成的命令包含对 CreationDate 和 LastEditDate 列的引用。 在下面的代码中,这些命令在 CustomersSyncAdapter 类的 OnInitialized 方法中进行重新定义。 由于 DeleteCommand() 属性不影响 CreationDate 或 LastEditDate 列,因此不进行重新定义。
每个 SQL 命令中的变量都用于在 Synchronization Services、客户端和服务器之间传递数据和元数据。 在下面的命令中使用了以下会话变量:
@sync\_row\_count:返回服务器上受上次操作影响的行数。 在 SQL Server 数据库中,@@rowcount 提供此变量的值。
@sync\_force\_write:用于强制进行因冲突或错误而无法进行的更改。
@sync\_last\_received\_anchor:用于定义要在会话过程中同步的一组更改。
有关会话变量的更多信息,请参见 How to: Use Session Variables(如何:使用会话变量)。
从同步命令中移除跟踪列
在 NorthwindCacheServerSyncProvider 类的 End Class 语句之后,将下面的代码添加到 NorthwindCache 类(NorthwindCache.vb 或 NorthwindCache.cs)。
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 Program Common Client and Server Synchronization Tasks(如何对常见客户端和服务器同步任务进行编程)。
请参见
概念
其他资源
How to Program Common Client and Server Synchronization Tasks(如何对常见客户端与服务器同步任务进行编程)