Como manipular conflitos de dados e erros de sincronização de colaboração (SQL Server)
Este tópico mostra como manipular conflitos de dados e erros ao usar o Sync Framework para sincronizar bancos de dados do SQL Server e do SQL Server Compact. Os exemplos neste tópico se concentram nos seguintes eventos e tipos do Sync Framework:
Para obter mais informações sobre como executar o código de exemplo, consulte "Exemplo de aplicativos nos tópicos de instruções" em Sincronizando o SQL Server e o SQL Server Compact.
Entendendo os conflitos de dados e erros
Nos provedores de banco de dados do Sync Framework, os conflitos e erros são detectados no nível da linha. Uma linha está em conflito quando foi alterada em mais de um nó entre as sincronizações. Normalmente, os erros que ocorrem durante a sincronização envolvem uma violação de restrição como uma chave primária duplicada. Se possível, os aplicativos devem ser projetados para evitar conflitos, uma vez que sua detecção e resolução adicionam complexidade, processamento e tráfego de rede. As maneiras mais comuns de evitar conflitos são: atualizar uma tabela em apenas um nó ou filtrar dados para que somente um nó atualize uma determinada linha. Em alguns aplicativos, os conflitos não podem ser evitados. Por exemplo, em um aplicativo de força de vendas, dois vendedores podem dividir um território. Os dois podem atualizar os dados para o mesmo cliente e as mesmas ordens. Por isso, o Sync Framework dispõe de um conjunto de recursos que os aplicativos podem usar para detectar e resolver conflitos.
Os conflitos de dados podem ocorrer em qualquer cenário de sincronização no qual as alterações sejam feitas em mais de um nó. Os conflitos podem ocorrer na sincronização bidirecional, assim como nas sincronizações somente para download e somente para carregamento. Por exemplo, se uma linha for excluída de um nó e a mesma linha for atualizada em outro nó, haverá um conflito quando o Sync Framework tentar carregar e aplicar as atualizações no primeiro nó.
Os conflitos ocorrem sempre entre os dois nós que estão sendo sincronizados atualmente. Considere o seguinte cenário:
O nó A e o nó B executam sincronização bidirecional com o nó C.
Uma linha é atualizada no nó A e, depois, o nó A sincroniza. Não há nenhum conflito, e a linha é aplicada no nó C.
A mesma linha é atualizada no nó B e, depois, o nó B sincroniza. Agora, a linha do nó B está em conflito com a linha do nó C devido à atualização originada no nó A.
Se você resolver esse conflito a favor do nó C, o Sync Framework poderá aplicar a linha do nó C no nó B. Se você resolver a favor do nó B, o Sync Framework poderá aplicar a linha do nó B no nó. Durante uma sincronização posterior entre o nó A e o nó C, a atualização originada no nó B será aplicada no nó A.
Tipos de Conflitos e Erros
O Sync Framework detecta os tipos de conflitos. Esses conflitos são definidos na enumeração DbConflictType:
Um conflito LocalInsertRemoteInsert ocorre quando dois nós inserirem uma linha que tem a mesma chave primária. Esse tipo de conflito também é conhecido como colisão de chave primária.
Um conflito LocalUpdateRemoteUpdate ocorre quando dois nós alteram a mesma linha. Esse é o tipo de conflito mais comum.
Os conflitos LocalUpdateRemoteDelete e LocalDeleteRemoteUpdate ocorrem quando um nó atualiza uma linha que foi excluída por outro nó.
Um conflito ErrorsOccurred ocorre quando um erro impede que uma linha seja aplicada.
Detecção de conflitos e erros
Se uma linha não puder ser aplicada durante a sincronização, normalmente será devido a um erro ou conflito de dados. Em ambos os casos, o evento ApplyChangeFailed é gerado. O provedor gera o erro para o nó no qual o conflito foi detectado. Por exemplo, se você especificar um valor de UploadAndDownload para a propriedade Direction, as alterações serão carregadas primeiro do provedor local para o provedor remoto. Nesse caso, o evento é criado pelo provedor que você especificou para a propriedade RemoteProvider. Se as alterações fossem primeiro baixadas e depois carregadas, o evento seria criado pelo provedor que você especificou para a propriedade LocalProvider. Seja qual for o provedor que gerou o evento e a localização dos componentes de sincronização, a alteração de dados no nó no qual o evento foi criado é considerada a alteração local (LocalChange) e a outra linha é considerada a alteração remota (RemoteChange). Isso difere da sincronização do cliente e do servidor, na qual ClientChange e ServerChange estão sempre associados, respectivamente, ao banco de dados do cliente e do servidor.
Depois que o evento ApplyChangeFailed é criado, as linhas conflitantes são selecionadas por um procedimento armazenado que o Sync Framework cria para cada tabela quando um banco de dados é provisionado para sincronização. Por padrão, esse procedimento é denominado <TableName>_selectrow
. O Sync Framework executa esse procedimento quando uma operação de inserção, atualização ou exclusão retorna um valor @sync_row_count de 0. Esse valor indica que houve falha na operação.
Resolução de conflitos e erros
A resolução de conflitos e erros deve ser usada em resposta ao evento ApplyChangeFailed. O objeto DbApplyChangeFailedEventArgs fornece acesso a várias propriedades que podem ser usadas durante a resolução de conflitos:
Especifique como resolver o conflito definindo a propriedade Action para um dos valores da enumeração ApplyAction:
Continue: ignorar o conflito e continuar a sincronização.
RetryApplyingRow e RetryNextSync: repetir a aplicação da linha. Haverá falha na nova tentativa, e o evento será gerado novamente se você não solucionar a causa do conflito, alterando uma ou ambas as linhas conflitantes.
RetryWithForceWrite: repetir com lógica para forçar a aplicação da alteração. A especificação dessa opção define a variável de sessão
@sync_force_write
como 1. A seção "Exemplos" deste tópico mostra como uma alteração remota é forçada a substituir uma alteração local com base na lógica do procedimento armazenado de atualização criado pelo Sync Framework.
Obtenha o tipo de conflito e exiba as linhas conflitantes de cada nó, usando a propriedade Conflict.
Obtenha o conjunto de dados das alterações que está sendo sincronizado usando a propriedade Context. As linhas que são expostas pela propriedade Conflict são cópias. Portanto, a substituição delas não altera as linhas aplicadas. Use o conjunto de dados exposto pela propriedade Context para desenvolver os esquemas de resolução personalizados se o aplicativo os exigir.
Dica
A API do Sync Framework contém um tipo e uma propriedade relacionados à resolução de conflitos, mas que não são usados nesta versão da API: DbResolveAction e ConflictResolutionPolicy.
Exemplos
Os exemplos de código a seguir mostram como configurar a detecção e a resolução de conflitos.
Principais partes da API
Esta seção fornece exemplos de código que destacam as partes principais da API usadas na resolução e detecção de conflitos. O exemplo de código a seguir mostra o procedimento armazenado que o Sync Framework usa para aplicar atualizações à tabela Customer
. Esse procedimento executa uma atualização com base no valor do parâmetro @sync_force_write
. Se a linha tiver sido atualizada no banco de dados local e o parâmetro for definido como 0, a atualização remota não será aplicada. Porém, se o parâmetro for definido como 1, a atualização remota substituirá a atualização local.
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
O exemplo de código a seguir mostra como podem ser processados conflitos de atualização-atualização em um manipulador de eventos ApplyChangeFailed
. No exemplo, as linhas conflitantes são exibidas no console com uma opção para especificar qual linha deveria ganhar o conflito. Se você executar o exemplo de código completo no fim deste tópico, verá dois conjuntos de linhas conflitantes: quando o nó 1 sincroniza com o nó 2 e quando o nó 2 sincroniza com o nó 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
O exemplo de código a seguir registra em um arquivo as informações de erro.
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()
Exemplo de código completo
O exemplo de código completo a seguir inclui os exemplos de código descritos anteriormente e código adicional para executar a sincronização. O exemplo requer a classe Utility
disponível em Classe de utilitário para tópicos de instruções do provedor de banco de dados.
// 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