Procédure : gérer les conflits de données et les erreurs pour la synchronisation de bases de données (SQL Server)
Cette rubrique vous montre comment gérer les conflits de données et les erreurs lors de l'utilisation de Sync Framework pour synchroniser les bases de données SQL Server et SQL Server Compact. Les exemples de cette rubrique reposent sur les types et les événements Sync Framework suivants :
ApplyChangeFailed événement, et DbApplyChangeFailedEventArgs
Pour plus d'informations sur le mode d'exécution d'un exemple de code, consultez « Exemples d'application dans les rubriques de procédures » dans Synchronisation de SQL Server et SQL Server Compact.
Présentation des conflits de données et des erreurs
Dans les fournisseurs de bases de données Sync Framework, les conflits et les erreurs sont détectés au niveau de la ligne. Une ligne est en conflit lorsqu'elle a été modifiée sur plusieurs nœuds entre les synchronisations. Les erreurs qui se produisent lors de la synchronisation impliquent en règle générale une violation de contrainte, telle qu'une clé primaire en double. Les applications doivent être conçues pour éviter les conflits dans la mesure du possible, car la détection et la résolution de conflit conduisent à une plus grande complexité et génèrent un traitement supplémentaire ainsi qu'un trafic réseau accru. Pour éviter les conflits, les méthodes les plus courantes sont les suivantes : mise à jour d'une table sur un seul nœud ou filtrage des données afin que seul un nœud mette à jour une ligne spécifique. Dans certaines applications, les conflits ne peuvent pas être évités. Par exemple, dans une application de force de vente, deux commerciaux peuvent partager un secteur de vente. Les deux commerciaux peuvent mettre à jour les données pour les mêmes client et commandes. Par conséquent, Sync Framework fournit un jeu de fonctionnalités que les applications peuvent utiliser pour détecter et résoudre des conflits.
Des conflits de données peuvent survenir dans les scénarios de synchronisation dans lesquels des modifications sont effectuées sur plusieurs nœuds. Des conflits peuvent se produire lors de la synchronisation bidirectionnelle, mais ils peuvent également se produire lors de la synchronisation par téléchargement ascendant uniquement et par téléchargement uniquement. Par exemple, si une ligne est supprimé sur un nœud et que cette même ligne est mise à jour sur un autre nœud, un conflit se produit lorsque Sync Framework tente de télécharger et d'appliquer la mise à jour sur le premier nœud.
Les conflits se produisent toujours entre les deux nœuds en cours de synchronisation. Examinez les cas suivants :
Le nœud A et le nœud B effectuent une synchronisation bidirectionnelle avec le nœud C.
Une ligne est mise à jour sur le nœud A, puis le nœud A est synchronisé. Aucun conflit ne se produit et la ligne est appliquée sur le nœud C.
La même ligne est mise à jour sur le nœud B, puis le nœud B est synchronisé. La ligne du nœud B est à présent en conflit avec la ligne du nœud C en raison de la mise à jour qui a été initialisée sur le nœud A.
Si vous résolvez ce conflit en faveur du nœud C, Sync Framework peut appliquer la ligne du nœud C au nœud B. Si vous résolvez ce conflit en faveur du nœud B, Sync Framework peut appliquer la ligne du nœud B sur le nœud. Lors d'une synchronisation ultérieure entre le client A et le nœud C, la mise à jour qui a été initialisée sur le nœud B est appliquée au nœud A.
Types de conflits et d'erreurs
Sync Framework détecte les types de conflits suivants. Ceux-ci sont définis dans l'énumération DbConflictType :
Un conflit LocalInsertRemoteInsert se produit lorsque deux nœuds insèrent une ligne qui a la même clé primaire. Ce type de conflit est également connu sous le nom de « collision de clé primaire ».
Un conflit LocalUpdateRemoteUpdate se produit lorsque deux nœuds modifient la même ligne. Il s'agit du type de conflit le plus courant.
Des conflits LocalUpdateRemoteDelete et LocalDeleteRemoteUpdate se produisent lorsqu'un nœud met à jour une ligne que l'autre nœud a supprimé.
Un conflit ErrorsOccurred se produit lorsqu'une erreur empêche l'application d'une ligne.
Détection des conflits et des erreurs
S'il est impossible d'appliquer une ligne durant la synchronisation, ce problème est généralement dû à une erreur ou à un conflit de données. Dans les deux cas, l'événement ApplyChangeFailed est déclenché. Le fournisseur déclenche l'erreur pour le nœud sur lequel le conflit est détecté. Par exemple, si vous spécifiez une valeur de UploadAndDownload pour la propriété Direction, les modifications sont téléchargées en premier du fournisseur local sur le fournisseur distant. Dans ce cas, l'événement est déclenché par le fournisseur que vous avez spécifié pour la propriété RemoteProvider. Si les modifications ont été tout d'abord téléchargées, puis transférées, l'événement serait déclenché par le fournisseur que vous avez spécifié pour la propriété LocalProvider. Quels que soient le fournisseur qui déclenche l'événement et l'emplacement des composants de synchronisation, la modification de données sur le nœud sur lequel l'événement est déclenché est considérée comme étant la modification locale (LocalChange), et l'autre ligne est considérée comme une modification distante (RemoteChange). Cette synchronisation diffère de la synchronisation client et serveur, dans laquelle ClientChange et ServerChange sont toujours associés à la base de données client et la base de données serveur, respectivement.
Après avoir déclenché l'événement ApplyChangeFailed, les lignes en conflit sont sélectionnées par une procédure stockée que Sync Framework crée pour chaque table lorsqu'une base de données est approvisionnée pour la synchronisation. Par défaut, cette procédure est nommée <TableName>_selectrow. Sync Framework exécute cette procédure lorsqu'une opération d'insertion, de mise à jour ou de suppression retourne une valeur @sync\_row\_count de 0. Cette valeur indique que l'opération a échoué.
Résolution des conflits et des erreurs
La résolution des conflits et des erreurs doit être gérée en réponse à l'événement ApplyChangeFailed. L'objet DbApplyChangeFailedEventArgs permet d'accéder à plusieurs propriétés qui peuvent être utilisées lors de la résolution des conflits :
Spécifiez le mode de résolution du conflit en définissant la propriété Action à l'une des valeurs de l'énumération ApplyAction suivantes :
Continue : ignorer le conflit et poursuivre la synchronisation.
RetryApplyingRow et RetryNextSync : retenter d'appliquer la ligne. La nouvelle tentative échouera et l'événement sera déclenché de nouveau si vous ne résolvez pas le conflit en modifiant l'une des lignes en conflit ou les deux lignes en conflit.
RetryWithForceWrite : réessayer avec la logique pour forcer l'application de la modification. La spécification de cette option affecte à la variable de session @sync\_force\_write la valeur 1. La section « Exemples » de cette rubrique illustre comment une modification distante est forcée de remplacer une modification locale selon la logique dans la procédure stockée de mise à jour que Sync Framework crée.
Obtenez le type de conflit et affichez les lignes en conflit de chaque nœud à l'aide de la propriété Conflict.
Obtenez le groupe de données des modifications faisant l'objet d'une synchronisation à l'aide de la propriété Context. Les lignes exposées par la propriété Conflict sont des copies. Par conséquent, leur remplacement ne modifie pas les lignes qui sont appliquées. Utilisez le groupe de données exposé par la propriété Context pour développer des schémas de résolution personnalisés si ces derniers sont requis par l'application.
Notes
L'API de Sync Framework contient un type et une propriété liés à la résolution de conflit, mais qui ne sont pas utilisés dans cette version de l'API : DbResolveAction et ConflictResolutionPolicy.
Exemples
Les exemples de code suivants indiquent comment configurer la détection de conflit et la résolution.
Éléments clés de l'API
Cette section contient des exemples de code qui désignent les éléments clés de l'API utilisés pour la détection et la résolution de conflit. L'exemple de code suivant affiche la procédure stockée que Sync Framework utilise pour appliquer des mises à jour à la table Customer. Cette procédure effectue une mise à jour en fonction de la valeur du paramètre @sync\_force\_write. Si la ligne a été mise à jour dans la base de données locale et que le paramètre a la valeur 0, la mise à jour distante n'est pas appliquée. Toutefois, si le paramètre a la valeur 1, la mise à jour distante remplace la mise à jour locale.
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
L'exemple de code suivant montre comment les conflits de mise à jour-mise à jour peuvent être traités dans un gestionnaire d'événements ApplyChangeFailed. Dans l'exemple, les lignes en conflit sont affichées sur la console avec une option pour spécifier la ligne doit gagner le conflit. Si vous exécutez l'exemple de code complet à la fin de cette rubrique, vous constaterez deux jeux de lignes en conflit : lorsque node 1 est synchronisé avec node 2 et lorsque node 2 est synchronisé avec node 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
L'exemple de code suivant enregistre les informations d'erreur dans un fichier.
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()
Exemple de code complet
L'exemple de code complet ci-dessous inclut les exemples de code décrits précédemment, ainsi que du code supplémentaire pour effectuer la synchronisation. L'exemple requiert la classe Utility qui est disponible dans Classe d'utilitaire pour les rubriques de procédures sur le fournisseur de bases de données.
// 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(serverConn, scopeDesc);
serverConfig.SetCreateTableDefault(DbSyncCreationOption.Skip);
// Configure the scope and change-tracking infrastructure.
serverConfig.Apply();
// 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(clientSqlConn, clientSqlDesc);
clientSqlConfig.Apply();
// This database does not yet exist.
Utility.DeleteAndRecreateCompactDatabase(Utility.ConnStr_SqlCeSync1, true);
DbSyncScopeDescription clientSqlCeDesc = SqlSyncDescriptionBuilder.GetDescriptionForScope("customer", serverConn);
SqlCeSyncScopeProvisioning clientSqlCeConfig = new SqlCeSyncScopeProvisioning(clientSqlCe1Conn, clientSqlCeDesc);
clientSqlCeConfig.Apply();
// 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(serverConn, scopeDesc)
serverConfig.SetCreateTableDefault(DbSyncCreationOption.Skip)
' Configure the scope and change-tracking infrastructure.
serverConfig.Apply()
' 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(clientSqlConn, clientSqlDesc)
clientSqlConfig.Apply()
' 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(clientSqlCe1Conn, clientSqlCeDesc)
clientSqlCeConfig.Apply()
' 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