演练:扩展本地数据库缓存以支持双向同步
在 Visual Studio 2008 中,**“本地数据库缓存”配置 SQL Server Compact 数据库和支持 Sync Framework 的一组部分类。因为 Visual Studio 生成部分类,所以,您可以编写代码以便添加同步功能,同时仍能够在“配置数据同步”对话框中查看和更改设置。有关“本地数据库缓存”**和部分类的更多信息,请参见 Visual Studio 2008 文档。
默认情况下,**“配置数据同步”**对话框使您能够配置 Sync Framework 以便仅下载方案。这意味着,在您配置数据同步后,调用 Synchronize 将只将变更从服务器下载到客户端数据库。扩展同步代码的最常见方式之一是配置双向同步。这使您可以将变更从客户端上载到服务器。为了实现双向同步,建议您通过以下方法扩展生成的代码:
将同步方向设置为双向。
添加代码以处理同步冲突。
从同步命令中删除服务器跟踪列。
备注
Visual Studio 2008 在为“本地数据库缓存”生成代码时,使用 Sync Framework for ADO.NET 1.0。
必备条件
在开始本演练之前,必须在 Visual Studio 2008 文档中完成以下演练:“演练:创建偶尔连接的应用程序”。在完成该演练后,您将具有一个项目,该项目包含**“本地数据库缓存”**和一个 Windows 窗体应用程序,该应用程序可用于将变更从 Northwind Customers 表下载到 SQL Server Compact 数据库。您现在可以加载本演练解决方案和添加双向功能了。
打开 OCSWalkthrough 解决方案
打开 Visual Studio。
在**“文件”**菜单上,打开某一现有解决方案或项目并定位到 OCSWalkthrough 解决方案。这是 OCSWalkthrough.sln 文件。
设置同步方向
**“配置数据同步”**对话框将 SyncDirection 属性设置为 DownloadOnly 或 Snapshot。为了启用双向同步,对于您要启用以便上载变更的每个表,都将 SyncDirection 属性设置为 Bidirectional。
设置同步方向
右键单击 NorthwindCache.sync,然后选择**“查看代码”。如果您是首次这样做,则 Visual Studio 将在“解决方案资源管理器”**中的 NorthwindCache.sync 节点下创建一个 NorthwindCache 类文件。该文件包含一个
NorthwindCacheSyncAgent
部分类,并且您可以根据需要添加其他类。在 NorthwindCache 类文件中,将一行代码添加到
NorthwindCacheSyncAgent.OnInitialized()
方法中:public partial class NorthwindCacheSyncAgent { partial void OnInitialized() { this.Customers.SyncDirection = Microsoft.Synchronization.Data.SyncDirection.Bidirectional; } }
Partial Public Class NorthwindCacheSyncAgent Partial Sub OnInitialized() Me.Customers.SyncDirection = Microsoft.Synchronization.Data.SyncDirection.Bidirectional End Sub End Class
在代码编辑器中打开 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。
在该窗体中,更新一条记录,然后单击工具栏上的**“保存”**按钮。
单击**“立即同步”**。
将出现一个消息框,其中包含与同步的记录有关的信息。统计信息显示已上载一行和下载一行,即便没有对服务器进行任何变更。还发生其他的下载,因为来自客户端的变更将在它们在服务器上应用后返回到客户端。有关更多信息,请参见如何使用自定义变更跟踪系统中的“确定执行数据变更的客户端”。
单击**“确定”**以关闭该消息框,但保持应用程序运行。
同步和查看冲突解决
在该窗体中,更新一条记录,然后单击**“保存”**按钮。
在保持应用程序仍在运行的情况下,使用**“服务器资源管理器”/“数据库资源管理器”**(或者其他数据库管理工具)连接到服务器数据库。
为了演示用于冲突解决的默认行为,在**“服务器资源管理器”/“数据库资源管理器”**中,更新您已在该窗体中更新的同一记录,但更新为其他值,并且提交该变更。(离开已修改的行。)
返回到该窗体,然后单击**“立即同步”**。
确认应用程序网格和服务器数据库中的更新。请注意,您在服务器上进行的更新已覆盖在客户端上进行的更新。有关如何更改此冲突解决行为的信息,请参见本主题中的下一节“添加代码以处理同步冲突”。
添加代码以处理同步冲突
在 Sync Framework 中,如果在同步间在客户端和服务器上都已更改了某一行,则该行将处于冲突状态。Sync Framework 提供了一组功能,可用于发现和解决冲突。在本演练中,您将添加基本的冲突处理功能,以便解决在客户端和服务器上都更新同一行的情况下产生的冲突。其他类型的冲突包括在一个数据库中正删除某一行、而在另一个数据库中正更新该行,或者某些行将重复的主键插入两个数据库中。有关如何发现和解决冲突的更多信息,请参见如何处理数据冲突和错误。
添加冲突处理
添加代码以处理服务器 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 的AddHandlers
方法: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 命令中删除这些列。对于从服务器选择要应用于客户端的变更的命令,也可以从中删除这些列,但这不是必需的。由于对客户端数据库中的某些架构变更存在限制,因此不能删除这些列。有关同步命令的更多信息,请参见如何指定快照同步、下载同步、上载同步和双向同步。
备注
如果您使用 SQL Server 变更跟踪,则跟踪列将不添加到您的表中。在此情况下,您不必更改将变更应用于服务器的列。
从同步命令中删除跟踪列
将以下代码添加到
NorthwindCacheServerSyncProvider
类的End Class
语句后的NorthwindCache
类(NorthwindCache.vb 或 NorthwindCache.cs)。此代码将重新定义两个命令,这两个命令将设置为针对Customers
表的 SyncAdapter 对象的属性:InsertCommand 属性和 UpdateCommand 属性。已由**“配置数据同步”**对话框生成的命令包含了对CreationDate
列和LastEditDate
列的引用。这些命令已在CustomersSyncAdapter
类的OnInitialized
方法中重新定义。DeleteCommand 属性未重新定义,因为它不影响CreationDate
或LastEditDate
列。每个 SQL 命令中的变量都用于在 Sync Framework、客户端和服务器之间传递数据和元数据。以下会话变量用于下面的命令:
@sync_row_count
:返回服务器上受上一次操作影响的行数。在 SQL Server 数据库中,@@ROWCOUNT 提供此变量的值。@sync_force_write
:用于强制应用由于冲突或错误而未能应用的变更。@sync_last_received_anchor
:用于定义在会话期间要同步的变更集。
有关会话变量的更多信息,请参见如何使用会话变量。
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
列中更改某一值来更新一条记录,然后单击**“保存”**按钮。返回到该窗体,然后单击**“立即同步”**。
确认应用程序网格和服务器数据库中的更新。请注意,来自服务器的列值已覆盖在客户端上进行的更新。该更新过程如下所示:
Sync Framework 标识已在客户端更改的行。
在同步过程中,该行将上载到并应用于服务器数据库中的表。但是,跟踪列不包括在更新语句中。Sync Framework 将有效地对表执行“虚更新”。
该行现在将返回到客户端,但从服务器选择变更的命令将包括跟踪列。因此,已在客户端进行的变更将被来自服务器的值覆盖。
结论
在本演练中,您使用了基本的冲突处理配置了双向同步,并且解决了在客户端数据库中具有服务器跟踪列的潜在问题。通过使用部分类,可以在其他重要方面扩展**“本地数据库缓存”**代码。例如,您可以重新定义 SQL 命令,这些命令从服务器数据库中选择变更,以便在数据下载到客户端时筛选数据。我们建议您阅读本文档中的帮助主题,理解您可以添加或更改同步代码以满足您的应用程序要求的不同方式。有关详细信息,请参见对常见客户端与服务器同步任务进行编程。