如何处理协作同步中的数据冲突和错误 (SQL Server)
本主题说明当使用 Sync Framework 同步 SQL Server 和 SQL Server Compact 数据库时,如何处理数据冲突和错误。本主题中的示例着重介绍以下 Sync Framework 类型和事件:
有关如何运行示例代码的更多信息,请参见同步 SQL Server 和 SQL Server Compact中的“帮助主题中的示例应用程序”。
了解数据冲突和错误
在 Sync Framework 数据库提供程序中,将在行级别上检测冲突和错误。如果在两次同步之间在多个节点上变更了同一行,则该行便存在冲突。同步期间发生的错误通常涉及违反约束,如主键重复。在设计应用程序时便应避免产生冲突(如果能够做到这一点),因为冲突的检测和解决会增加应用程序的复杂性,增加处理负担和网络流量。避免冲突的一些最常见方法包括:只在一个节点上更新某个表;或者对数据进行筛选以便只有一个节点会对特定行进行更新。在某些应用程序中,冲突是不可避免的。例如,在销售应用程序中,两个销售人员可能共享一个区域。他们都能更新同一客户和订单的数据。因此,Sync Framework 提供了一组可供应用程序检测和解决冲突的功能。
在任何同步方案中,只要在多个节点上变更数据,便会发生数据冲突。在双向同步的情况下会产生冲突,但是,在只进行下载或上载的同步情况下也有可能产生冲突。例如,如果在某一节点上删除了某行,同时在其他节点上更新了这一行,那么当 Sync Framework 尝试上载和应用第一个节点上的更新时便会发生冲突。
冲突总是出现在当前进行同步的两个节点之间。请考虑以下情况:
节点 A 和节点 B 同时与节点 C 进行双向同步。
在节点 A 上更新了某行,然后对节点 A 进行同步。此时不会出现任何冲突,会在节点 C 上应用该行。
在节点 B 上更新同一行,然后对节点 B 进行同步。现在,来自节点 B 的该行与节点 C 上的该行发生了冲突,因为节点 A 已对该行进行了更新。
如果以节点 C 优先的方式解决此冲突,Sync Framework 会将节点 C 上的该行应用于节点 B。如果以节点 B 优先的方式解决冲突,则 Sync Framework 会将来自节点 B 的该行应用于节点 C。在节点 A 与节点 C 之间进行下一次同步时,源自节点 B 的更新将应用于节点 A。
冲突和错误的类型
Sync Framework 检测以下类型的冲突。这些类型定义在 DbConflictType 枚举中:
在两个节点插入的行带有相同主键时会发生 LocalInsertRemoteInsert 冲突。此类型的冲突又称为主键冲突。
两个节点对同一行进行变更时会发生 LocalUpdateRemoteUpdate 冲突。这是最常见的冲突类型。
当某一节点更新另一节点已经删除的行时,会发生 LocalUpdateRemoteDelete 和 LocalDeleteRemoteUpdate 冲突。
当某个错误阻止应用行时会发生 ErrorsOccurred 冲突。
冲突和错误的检测
如果在同步期间无法应用某行,通常是由于发生了错误或数据冲突。这两种情况均会引发 ApplyChangeFailed 事件。提供程序会在检测到冲突的节点引发错误。例如,如果为 Direction 属性指定了值 UploadAndDownload,将首先从本地提供程序将变更上载到远程提供程序。在这种情况下,您为 RemoteProvider 属性指定的提供程序将引发事件。如果首先下载变更然后再上载变更,将由为 LocalProvider 属性指定的提供程序引发该事件。不论由哪一个提供程序引发事件,也不论同步组件位于何处,都会将引发事件的节点上的数据变更视为本地变更 (LocalChange),而将另一行视为远程变更 (RemoteChange)。这与客户端和服务器同步不同,后者的 ClientChange 和 ServerChange 始终分别与客户端数据库和服务器数据库关联。
引发 ApplyChangeFailed 事件后,通过一个存储过程来选择发生冲突的行,该存储过程是在为同步设置数据库时 Sync Framework 为每个表创建的。默认情况下,将该过程命名为 <TableName>_selectrow
。当插入、更新或删除操作返回为 0 的 @sync_row_count 值时,Sync Framework 执行该过程。此值指示操作失败。
冲突和错误的解决
应该根据 ApplyChangeFailed 事件来相应地解决冲突和错误。DbApplyChangeFailedEventArgs 对象提供对冲突解决过程中可使用的多个属性的访问:
通过将 Action 属性设置为 ApplyAction 枚举值之一可指定如何解决冲突:
Continue:忽略冲突并继续执行同步。
RetryApplyingRow 和 RetryNextSync:重新尝试应用该行。如果没有通过变更存在冲突的行之一(或二者)来解决导致冲突的原因,则重试操作将失败,并将再次引发该事件。
RetryWithForceWrite:重新尝试逻辑以强制应用变更。如果指定此选项,会将会话变量
@sync_force_write
设置为 1。本主题的“示例”一节演示如何基于 Sync Framework 创建的 update 存储过程中的逻辑强制用远程变更覆盖本地变更。
通过使用 Conflict 属性获取冲突类型并查看来自各个节点的发生冲突的行。
通过使用 Context 属性获取要同步的变更数据集。通过 Conflict 属性公开的行是副本行。因此,覆盖这些行不会变更所应用的行。使用由 Context 属性公开的数据集可开发自定义解决方案(如果应用程序需要)。
备注
Sync Framework API 中有一个类型和一个属性与冲突解决相关,但在本版本的 API 中未使用,它们是 DbResolveAction 和 ConflictResolutionPolicy。
示例
下面的代码示例演示如何配置冲突检测和冲突解决。
API 的要点
本节提供了一些代码示例,指出了在冲突检测和冲突解决中使用的 API 的要点。下面的代码示例演示 Sync Framework 用于将更新应用到 Customer
表的存储过程。此过程基于 @sync_force_write
参数的值执行更新。如果该行已在本地数据库中更新并且参数设置为 0,则不应用远程更新。不过,如果参数设置为 1,远程更新将覆盖本地更新。
CREATE PROCEDURE [Sales].[Customer_update]
@CustomerId UniqueIdentifier,
@CustomerName NVarChar(100),
@SalesPerson NVarChar(100),
@CustomerType NVarChar(100),
@sync_force_write Int,
@sync_min_timestamp BigInt,
@sync_row_count Int OUTPUT
AS
BEGIN
UPDATE [Sales].[Customer] SET [CustomerName] = @CustomerName,
[SalesPerson] = @SalesPerson, [CustomerType] = @CustomerType FROM
[Sales].[Customer] [base] JOIN [Sales].[Customer_tracking] [side] ON
[base].[CustomerId] = [side].[CustomerId] WHERE
([side].[local_update_peer_timestamp] <= @sync_min_timestamp OR
@sync_force_write = 1) AND ([base].[CustomerId] = @CustomerId); SET
@sync_row_count = @@ROWCOUNT;
END
GO
下面的代码示例说明如何在 ApplyChangeFailed
事件处理程序中处理“更新-更新”冲突。在本例中,冲突行显示在控制台中,同时有一个选项指定哪一行应在冲突中入选。如果运行本主题结尾处的完整代码示例,您将看到两组冲突行(分别在节点 1 与节点 2 同步时以及节点 2 与节点 3 同步时出现)。
if (e.Conflict.Type == DbConflictType.LocalUpdateRemoteUpdate)
{
//Get the conflicting changes from the Conflict object
//and display them. The Conflict object holds a copy
//of the changes; updates to this object will not be
//applied. To make changes, use the Context object.
DataTable conflictingRemoteChange = e.Conflict.RemoteChange;
DataTable conflictingLocalChange = e.Conflict.LocalChange;
int remoteColumnCount = conflictingRemoteChange.Columns.Count;
int localColumnCount = conflictingLocalChange.Columns.Count;
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine("Row from database " + DbConflictDetected);
Console.Write(" | ");
//Display the local row. As mentioned above, this is the row
//from the database at which the conflict was detected.
for (int i = 0; i < localColumnCount; i++)
{
Console.Write(conflictingLocalChange.Rows[0][i] + " | ");
}
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine("Row from database " + DbOther);
Console.Write(" | ");
//Display the remote row.
for (int i = 0; i < remoteColumnCount; i++)
{
Console.Write(conflictingRemoteChange.Rows[0][i] + " | ");
}
//Ask for a conflict resolution option.
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine("Enter a resolution option for this conflict:");
Console.WriteLine("A = change from " + DbConflictDetected + " wins.");
Console.WriteLine("B = change from " + DbOther + " wins.");
string conflictResolution = Console.ReadLine();
conflictResolution.ToUpper();
if (conflictResolution == "A")
{
e.Action = ApplyAction.Continue;
}
else if (conflictResolution == "B")
{
e.Action = ApplyAction.RetryWithForceWrite;
}
else
{
Console.WriteLine(String.Empty);
Console.WriteLine("Not a valid resolution option.");
}
}
If e.Conflict.Type = DbConflictType.LocalUpdateRemoteUpdate Then
'Get the conflicting changes from the Conflict object
'and display them. The Conflict object holds a copy
'of the changes; updates to this object will not be
'applied. To make changes, use the Context object.
Dim conflictingRemoteChange As DataTable = e.Conflict.RemoteChange
Dim conflictingLocalChange As DataTable = e.Conflict.LocalChange
Dim remoteColumnCount As Integer = conflictingRemoteChange.Columns.Count
Dim localColumnCount As Integer = conflictingLocalChange.Columns.Count
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine("Row from database " & DbConflictDetected)
Console.Write(" | ")
'Display the local row. As mentioned above, this is the row
'from the database at which the conflict was detected.
For i As Integer = 0 To localColumnCount - 1
Console.Write(conflictingLocalChange.Rows(0)(i).ToString() & " | ")
Next
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine("Row from database " & DbOther)
Console.Write(" | ")
'Display the remote row.
For i As Integer = 0 To remoteColumnCount - 1
Console.Write(conflictingRemoteChange.Rows(0)(i).ToString() & " | ")
Next
'Ask for a conflict resolution option.
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine("Enter a resolution option for this conflict:")
Console.WriteLine("A = change from " & DbConflictDetected & " wins.")
Console.WriteLine("B = change from " & DbOther & " wins.")
Dim conflictResolution As String = Console.ReadLine()
conflictResolution.ToUpper()
If conflictResolution = "A" Then
e.Action = ApplyAction.Continue
ElseIf conflictResolution = "B" Then
e.Action = ApplyAction.RetryWithForceWrite
Else
Console.WriteLine([String].Empty)
Console.WriteLine("Not a valid resolution option.")
End If
下面的代码示例将错误信息记录到文件中。
else if (e.Conflict.Type == DbConflictType.ErrorsOccurred)
{
string logFile = @"C:\SyncErrorLog.txt";
Console.WriteLine(String.Empty);
Console.WriteLine("An error occurred during synchronization.");
Console.WriteLine("This error has been logged to " + logFile + ".");
StreamWriter streamWriter = File.AppendText(logFile);
StringBuilder outputText = new StringBuilder();
outputText.AppendLine("** APPLY CHANGE FAILURE AT " + DbConflictDetected.ToUpper() + " **");
outputText.AppendLine("Error source: " + e.Error.Source);
outputText.AppendLine("Error message: " + e.Error.Message);
streamWriter.WriteLine(DateTime.Now.ToShortTimeString() + " | " + outputText.ToString());
streamWriter.Flush();
streamWriter.Dispose();
}
ElseIf e.Conflict.Type = DbConflictType.ErrorsOccurred Then
Dim logFile As String = "C:\SyncErrorLog.txt"
Console.WriteLine([String].Empty)
Console.WriteLine("An error occurred during synchronization.")
Console.WriteLine("This error has been logged to " & logFile & ".")
Dim streamWriter As StreamWriter = File.AppendText(logFile)
Dim outputText As New StringBuilder()
outputText.AppendLine("** APPLY CHANGE FAILURE AT " & DbConflictDetected.ToUpper() & " **")
outputText.AppendLine("Error source: " & e.[Error].Source)
outputText.AppendLine("Error message: " & e.[Error].Message)
streamWriter.WriteLine((DateTime.Now.ToShortTimeString() & " | ") + outputText.ToString())
streamWriter.Flush()
streamWriter.Dispose()
完整的代码示例
下面的完整代码示例包括了上面介绍的代码示例以及用于执行同步的其他代码。示例需要 Utility
类,可通过用于数据库提供程序帮助主题的 Utility 类获得该类。
// NOTE: Before running this application, run the database sample script that is
// available in the documentation. The script drops and re-creates the tables that
// are used in the code, and ensures that synchronization objects are dropped so that
// Sync Framework can re-create them.
using System;
using System.IO;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
using Microsoft.Synchronization;
using Microsoft.Synchronization.Data;
using Microsoft.Synchronization.Data.SqlServer;
using Microsoft.Synchronization.Data.SqlServerCe;
namespace Microsoft.Samples.Synchronization
{
class Program
{
static void Main(string[] args)
{
// Create the connections over which provisioning and synchronization
// are performed. The Utility class handles all functionality that is not
//directly related to synchronization, such as holding connection
//string information and making changes to the server database.
SqlConnection serverConn = new SqlConnection(Utility.ConnStr_SqlSync_Server);
SqlConnection clientSqlConn = new SqlConnection(Utility.ConnStr_SqlSync_Client);
SqlCeConnection clientSqlCe1Conn = new SqlCeConnection(Utility.ConnStr_SqlCeSync1);
// Create a scope named "customer", and add the Customer table to the scope.
// GetDescriptionForTable gets the schema of the table, so that tracking
// tables and triggers can be created for that table.
DbSyncScopeDescription scopeDesc = new DbSyncScopeDescription("customer");
scopeDesc.Tables.Add(
SqlSyncDescriptionBuilder.GetDescriptionForTable("Sales.Customer", serverConn));
// Create a provisioning object for "customer" and specify that
// base tables should not be created (They already exist in SyncSamplesDb_SqlPeer1).
SqlSyncScopeProvisioning serverConfig = new SqlSyncScopeProvisioning(scopeDesc);
serverConfig.SetCreateTableDefault(DbSyncCreationOption.Skip);
// Configure the scope and change-tracking infrastructure.
serverConfig.Apply(serverConn);
// Retrieve scope information from the server and use the schema that is retrieved
// to provision the SQL Server and SQL Server Compact client databases.
// This database already exists on the server.
DbSyncScopeDescription clientSqlDesc = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn);
SqlSyncScopeProvisioning clientSqlConfig = new SqlSyncScopeProvisioning(clientSqlDesc);
clientSqlConfig.Apply(clientSqlConn);
// This database does not yet exist.
Utility.DeleteAndRecreateCompactDatabase(Utility.ConnStr_SqlCeSync1, true);
DbSyncScopeDescription clientSqlCeDesc = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn);
SqlCeSyncScopeProvisioning clientSqlCeConfig = new SqlCeSyncScopeProvisioning(clientSqlCeDesc);
clientSqlCeConfig.Apply(clientSqlCe1Conn);
// Initial synchronization sessions.
SampleSyncOrchestrator syncOrchestrator;
SyncOperationStatistics syncStats;
// Data is downloaded from the server to the SQL Server client.
syncOrchestrator = new SampleSyncOrchestrator(
new SqlSyncProvider("customer", clientSqlConn),
new SqlSyncProvider("customer", serverConn)
);
syncStats = syncOrchestrator.Synchronize();
syncOrchestrator.DisplayStats(syncStats, "initial");
// Data is downloaded from the SQL Server client to the
// SQL Server Compact client.
syncOrchestrator = new SampleSyncOrchestrator(
new SqlCeSyncProvider("customer", clientSqlCe1Conn),
new SqlSyncProvider("customer", clientSqlConn)
);
syncStats = syncOrchestrator.Synchronize();
syncOrchestrator.DisplayStats(syncStats, "initial");
// Make conflicting changes in two databases.
Utility.MakeConflictingChangeOnNode(Utility.ConnStr_SqlSync_Client, "Customer");
Utility.MakeConflictingChangeOnNode(Utility.ConnStr_SqlSync_Server, "Customer");
// Subsequent synchronization sessions.
syncOrchestrator = new SampleSyncOrchestrator(
new SqlSyncProvider("customer", clientSqlConn),
new SqlSyncProvider("customer", serverConn)
);
syncStats = syncOrchestrator.Synchronize();
syncOrchestrator.DisplayStats(syncStats, "subsequent");
syncOrchestrator = new SampleSyncOrchestrator(
new SqlCeSyncProvider("customer", clientSqlCe1Conn),
new SqlSyncProvider("customer", clientSqlConn)
);
syncStats = syncOrchestrator.Synchronize();
syncOrchestrator.DisplayStats(syncStats, "subsequent");
//Make a change in SyncSamplesDb_Peer2 that will fail when it
//is synchronized with SyncSamplesDb_Peer1.
Utility.MakeFailingChangeOnNode(Utility.ConnStr_SqlSync_Client);
// Subsequent synchronization sessions.
syncOrchestrator = new SampleSyncOrchestrator(
new SqlSyncProvider("customer", clientSqlConn),
new SqlSyncProvider("customer", serverConn)
);
syncStats = syncOrchestrator.Synchronize();
syncOrchestrator.DisplayStats(syncStats, "subsequent");
syncOrchestrator = new SampleSyncOrchestrator(
new SqlCeSyncProvider("customer", clientSqlCe1Conn),
new SqlSyncProvider("customer", clientSqlConn)
);
syncStats = syncOrchestrator.Synchronize();
syncOrchestrator.DisplayStats(syncStats, "subsequent");
//Exit.
Console.Write("\nPress Enter to close the window.");
Console.ReadLine();
}
}
public class SampleSyncOrchestrator : SyncOrchestrator
{
//Create class-level variables so that the ApplyChangeFailedEvent
//handler can use them.
private string _localProviderDatabase;
private string _remoteProviderDatabase;
public SampleSyncOrchestrator(RelationalSyncProvider localProvider, RelationalSyncProvider remoteProvider)
{
this.LocalProvider = localProvider;
this.RemoteProvider = remoteProvider;
this.Direction = SyncDirectionOrder.UploadAndDownload;
_localProviderDatabase = localProvider.Connection.Database.ToString();
_remoteProviderDatabase = remoteProvider.Connection.Database.ToString();
//Specify event handlers for the ApplyChangeFailed event for each provider.
//The handlers are used to resolve conflicting rows and log error information.
localProvider.ApplyChangeFailed += new EventHandler<DbApplyChangeFailedEventArgs>(dbProvider_ApplyChangeFailed);
remoteProvider.ApplyChangeFailed += new EventHandler<DbApplyChangeFailedEventArgs>(dbProvider_ApplyChangeFailed);
}
public void DisplayStats(SyncOperationStatistics syncStatistics, string syncType)
{
Console.WriteLine(String.Empty);
if (syncType == "initial")
{
Console.WriteLine("****** Initial Synchronization ******");
}
else if (syncType == "subsequent")
{
Console.WriteLine("***** Subsequent Synchronization ****");
}
Console.WriteLine("Start Time: " + syncStatistics.SyncStartTime);
Console.WriteLine("Total Changes Uploaded: " + syncStatistics.UploadChangesTotal);
Console.WriteLine("Total Changes Downloaded: " + syncStatistics.DownloadChangesTotal);
Console.WriteLine("Complete Time: " + syncStatistics.SyncEndTime);
Console.WriteLine(String.Empty);
}
private void dbProvider_ApplyChangeFailed(object sender, DbApplyChangeFailedEventArgs e)
{
//For conflict detection, the "local" database is the one at which the
//ApplyChangeFailed event occurs. We determine at which database the event
//fired and then compare the name of that database to the names of
//the databases specified as the LocalProvider and RemoteProvider.
string DbConflictDetected = e.Connection.Database.ToString();
string DbOther;
DbOther = DbConflictDetected == _localProviderDatabase ? _remoteProviderDatabase : _localProviderDatabase;
Console.WriteLine(String.Empty);
Console.WriteLine("Conflict of type " + e.Conflict.Type + " was detected at " + DbConflictDetected + ".");
if (e.Conflict.Type == DbConflictType.LocalUpdateRemoteUpdate)
{
//Get the conflicting changes from the Conflict object
//and display them. The Conflict object holds a copy
//of the changes; updates to this object will not be
//applied. To make changes, use the Context object.
DataTable conflictingRemoteChange = e.Conflict.RemoteChange;
DataTable conflictingLocalChange = e.Conflict.LocalChange;
int remoteColumnCount = conflictingRemoteChange.Columns.Count;
int localColumnCount = conflictingLocalChange.Columns.Count;
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine("Row from database " + DbConflictDetected);
Console.Write(" | ");
//Display the local row. As mentioned above, this is the row
//from the database at which the conflict was detected.
for (int i = 0; i < localColumnCount; i++)
{
Console.Write(conflictingLocalChange.Rows[0][i] + " | ");
}
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine("Row from database " + DbOther);
Console.Write(" | ");
//Display the remote row.
for (int i = 0; i < remoteColumnCount; i++)
{
Console.Write(conflictingRemoteChange.Rows[0][i] + " | ");
}
//Ask for a conflict resolution option.
Console.WriteLine(String.Empty);
Console.WriteLine(String.Empty);
Console.WriteLine("Enter a resolution option for this conflict:");
Console.WriteLine("A = change from " + DbConflictDetected + " wins.");
Console.WriteLine("B = change from " + DbOther + " wins.");
string conflictResolution = Console.ReadLine();
conflictResolution.ToUpper();
if (conflictResolution == "A")
{
e.Action = ApplyAction.Continue;
}
else if (conflictResolution == "B")
{
e.Action = ApplyAction.RetryWithForceWrite;
}
else
{
Console.WriteLine(String.Empty);
Console.WriteLine("Not a valid resolution option.");
}
}
//Write any errors to a log file.
else if (e.Conflict.Type == DbConflictType.ErrorsOccurred)
{
string logFile = @"C:\SyncErrorLog.txt";
Console.WriteLine(String.Empty);
Console.WriteLine("An error occurred during synchronization.");
Console.WriteLine("This error has been logged to " + logFile + ".");
StreamWriter streamWriter = File.AppendText(logFile);
StringBuilder outputText = new StringBuilder();
outputText.AppendLine("** APPLY CHANGE FAILURE AT " + DbConflictDetected.ToUpper() + " **");
outputText.AppendLine("Error source: " + e.Error.Source);
outputText.AppendLine("Error message: " + e.Error.Message);
streamWriter.WriteLine(DateTime.Now.ToShortTimeString() + " | " + outputText.ToString());
streamWriter.Flush();
streamWriter.Dispose();
}
}
}
}
' NOTE: Before running this application, run the database sample script that is
' available in the documentation. The script drops and re-creates the tables that
' are used in the code, and ensures that synchronization objects are dropped so that
' Sync Framework can re-create them.
Imports System
Imports System.IO
Imports System.Text
Imports System.Data
Imports System.Data.SqlClient
Imports System.Data.SqlServerCe
Imports Microsoft.Synchronization
Imports Microsoft.Synchronization.Data
Imports Microsoft.Synchronization.Data.SqlServer
Imports Microsoft.Synchronization.Data.SqlServerCe
Class Program
Public Shared Sub Main(ByVal args As String())
' Create the connections over which provisioning and synchronization
' are performed. The Utility class handles all functionality that is not
'directly related to synchronization, such as holding connection
'string information and making changes to the server database.
Dim serverConn As New SqlConnection(Utility.ConnStr_SqlSync_Server)
Dim clientSqlConn As New SqlConnection(Utility.ConnStr_SqlSync_Client)
Dim clientSqlCe1Conn As New SqlCeConnection(Utility.ConnStr_SqlCeSync1)
' Create a scope named "customer", and add the Customer table to the scope.
' GetDescriptionForTable gets the schema of the table, so that tracking
' tables and triggers can be created for that table.
Dim scopeDesc As New DbSyncScopeDescription("customer")
scopeDesc.Tables.Add(SqlSyncDescriptionBuilder.GetDescriptionForTable("Sales.Customer", serverConn))
' Create a provisioning object for "customer" and specify that
' base tables should not be created (They already exist in SyncSamplesDb_SqlPeer1).
Dim serverConfig As New SqlSyncScopeProvisioning(scopeDesc)
serverConfig.SetCreateTableDefault(DbSyncCreationOption.Skip)
' Configure the scope and change-tracking infrastructure.
serverConfig.Apply(serverConn)
' Retrieve scope information from the server and use the schema that is retrieved
' to provision the SQL Server and SQL Server Compact client databases.
' This database already exists on the server.
Dim clientSqlDesc As DbSyncScopeDescription = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn)
Dim clientSqlConfig As New SqlSyncScopeProvisioning(clientSqlDesc)
clientSqlConfig.Apply(clientSqlConn)
' This database does not yet exist.
Utility.DeleteAndRecreateCompactDatabase(Utility.ConnStr_SqlCeSync1, True)
Dim clientSqlCeDesc As DbSyncScopeDescription = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn)
Dim clientSqlCeConfig As New SqlCeSyncScopeProvisioning(clientSqlCeDesc)
clientSqlCeConfig.Apply(clientSqlCe1Conn)
' Initial synchronization sessions.
Dim syncOrchestrator As SampleSyncOrchestrator
Dim syncStats As SyncOperationStatistics
' Data is downloaded from the server to the SQL Server client.
syncOrchestrator = New SampleSyncOrchestrator(New SqlSyncProvider("customer", clientSqlConn), New SqlSyncProvider("customer", serverConn))
syncStats = syncOrchestrator.Synchronize()
syncOrchestrator.DisplayStats(syncStats, "initial")
' Data is downloaded from the SQL Server client to the
' SQL Server Compact client.
syncOrchestrator = New SampleSyncOrchestrator(New SqlCeSyncProvider("customer", clientSqlCe1Conn), New SqlSyncProvider("customer", clientSqlConn))
syncStats = syncOrchestrator.Synchronize()
syncOrchestrator.DisplayStats(syncStats, "initial")
' Make conflicting changes in two databases.
Utility.MakeConflictingChangeOnNode(Utility.ConnStr_SqlSync_Client, "Customer")
Utility.MakeConflictingChangeOnNode(Utility.ConnStr_SqlSync_Server, "Customer")
' Subsequent synchronization sessions.
syncOrchestrator = New SampleSyncOrchestrator(New SqlSyncProvider("customer", clientSqlConn), New SqlSyncProvider("customer", serverConn))
syncStats = syncOrchestrator.Synchronize()
syncOrchestrator.DisplayStats(syncStats, "subsequent")
syncOrchestrator = New SampleSyncOrchestrator(New SqlCeSyncProvider("customer", clientSqlCe1Conn), New SqlSyncProvider("customer", clientSqlConn))
syncStats = syncOrchestrator.Synchronize()
syncOrchestrator.DisplayStats(syncStats, "subsequent")
'Make a change in SyncSamplesDb_Peer2 that will fail when it
'is synchronized with SyncSamplesDb_Peer1.
Utility.MakeFailingChangeOnNode(Utility.ConnStr_SqlSync_Client)
' Subsequent synchronization sessions.
syncOrchestrator = New SampleSyncOrchestrator(New SqlSyncProvider("customer", clientSqlConn), New SqlSyncProvider("customer", serverConn))
syncStats = syncOrchestrator.Synchronize()
syncOrchestrator.DisplayStats(syncStats, "subsequent")
syncOrchestrator = New SampleSyncOrchestrator(New SqlCeSyncProvider("customer", clientSqlCe1Conn), New SqlSyncProvider("customer", clientSqlConn))
syncStats = syncOrchestrator.Synchronize()
syncOrchestrator.DisplayStats(syncStats, "subsequent")
'Exit.
Console.Write(vbLf & "Press Enter to close the window.")
Console.ReadLine()
End Sub
End Class
Public Class SampleSyncOrchestrator
Inherits SyncOrchestrator
'Create class-level variables so that the ApplyChangeFailedEvent
'handler can use them.
Private _localProviderDatabase As String
Private _remoteProviderDatabase As String
Public Sub New(ByVal localProvider As RelationalSyncProvider, ByVal remoteProvider As RelationalSyncProvider)
Me.LocalProvider = localProvider
Me.RemoteProvider = remoteProvider
Me.Direction = SyncDirectionOrder.UploadAndDownload
_localProviderDatabase = localProvider.Connection.Database.ToString()
_remoteProviderDatabase = remoteProvider.Connection.Database.ToString()
'Specify event handlers for the ApplyChangeFailed event for each provider.
'The handlers are used to resolve conflicting rows and log error information.
AddHandler localProvider.ApplyChangeFailed, AddressOf dbProvider_ApplyChangeFailed
AddHandler remoteProvider.ApplyChangeFailed, AddressOf dbProvider_ApplyChangeFailed
End Sub
Public Sub DisplayStats(ByVal syncStatistics As SyncOperationStatistics, ByVal syncType As String)
Console.WriteLine([String].Empty)
If syncType = "initial" Then
Console.WriteLine("****** Initial Synchronization ******")
ElseIf syncType = "subsequent" Then
Console.WriteLine("***** Subsequent Synchronization ****")
End If
Console.WriteLine("Start Time: " & syncStatistics.SyncStartTime)
Console.WriteLine("Total Changes Uploaded: " & syncStatistics.UploadChangesTotal)
Console.WriteLine("Total Changes Downloaded: " & syncStatistics.DownloadChangesTotal)
Console.WriteLine("Complete Time: " & syncStatistics.SyncEndTime)
Console.WriteLine([String].Empty)
End Sub
Private Sub dbProvider_ApplyChangeFailed(ByVal sender As Object, ByVal e As DbApplyChangeFailedEventArgs)
'For conflict detection, the "local" database is the one at which the
'ApplyChangeFailed event occurs. We determine at which database the event
'fired and then compare the name of that database to the names of
'the databases specified as the LocalProvider and RemoteProvider.
Dim DbConflictDetected As String = e.Connection.Database.ToString()
Dim DbOther As String
DbOther = If(DbConflictDetected = _localProviderDatabase, _remoteProviderDatabase, _localProviderDatabase)
Console.WriteLine([String].Empty)
Console.WriteLine(("Conflict of type " & e.Conflict.Type & " was detected at ") + DbConflictDetected & ".")
If e.Conflict.Type = DbConflictType.LocalUpdateRemoteUpdate Then
'Get the conflicting changes from the Conflict object
'and display them. The Conflict object holds a copy
'of the changes; updates to this object will not be
'applied. To make changes, use the Context object.
Dim conflictingRemoteChange As DataTable = e.Conflict.RemoteChange
Dim conflictingLocalChange As DataTable = e.Conflict.LocalChange
Dim remoteColumnCount As Integer = conflictingRemoteChange.Columns.Count
Dim localColumnCount As Integer = conflictingLocalChange.Columns.Count
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine("Row from database " & DbConflictDetected)
Console.Write(" | ")
'Display the local row. As mentioned above, this is the row
'from the database at which the conflict was detected.
For i As Integer = 0 To localColumnCount - 1
Console.Write(conflictingLocalChange.Rows(0)(i).ToString() & " | ")
Next
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine("Row from database " & DbOther)
Console.Write(" | ")
'Display the remote row.
For i As Integer = 0 To remoteColumnCount - 1
Console.Write(conflictingRemoteChange.Rows(0)(i).ToString() & " | ")
Next
'Ask for a conflict resolution option.
Console.WriteLine([String].Empty)
Console.WriteLine([String].Empty)
Console.WriteLine("Enter a resolution option for this conflict:")
Console.WriteLine("A = change from " & DbConflictDetected & " wins.")
Console.WriteLine("B = change from " & DbOther & " wins.")
Dim conflictResolution As String = Console.ReadLine()
conflictResolution.ToUpper()
If conflictResolution = "A" Then
e.Action = ApplyAction.Continue
ElseIf conflictResolution = "B" Then
e.Action = ApplyAction.RetryWithForceWrite
Else
Console.WriteLine([String].Empty)
Console.WriteLine("Not a valid resolution option.")
End If
'Write any errors to a log file.
ElseIf e.Conflict.Type = DbConflictType.ErrorsOccurred Then
Dim logFile As String = "C:\SyncErrorLog.txt"
Console.WriteLine([String].Empty)
Console.WriteLine("An error occurred during synchronization.")
Console.WriteLine("This error has been logged to " & logFile & ".")
Dim streamWriter As StreamWriter = File.AppendText(logFile)
Dim outputText As New StringBuilder()
outputText.AppendLine("** APPLY CHANGE FAILURE AT " & DbConflictDetected.ToUpper() & " **")
outputText.AppendLine("Error source: " & e.[Error].Source)
outputText.AppendLine("Error message: " & e.[Error].Message)
streamWriter.WriteLine((DateTime.Now.ToShortTimeString() & " | ") + outputText.ToString())
streamWriter.Flush()
streamWriter.Dispose()
End If
End Sub
End Class