Как обрабатывать конфликты и ошибки в данных для синхронизации совместной работы (SQL Server)

В этом разделе описаны методы обработки конфликтов данных и ошибок при использовании платформы Sync Framework для синхронизации баз данных SQL Server и SQL Server Compact. В примерах этого раздела обсуждаются следующие типы и события платформы Sync Framework.

Дополнительные сведения о запуске образца кода см. в подразделе «Образцы приложений в разделах инструкций» раздела Синхронизация SQL Server и SQL Server Compact.

Основные сведения об ошибках и конфликтах данных

В поставщиках баз данных платформы Sync Framework обнаружение конфликтов и ошибок выполняется на уровне строк. В строке возникает конфликт, если она изменялась между сеансами синхронизации на нескольких узлах. Ошибки, возникающие во время синхронизации, обычно связаны с нарушением ограничений, например с повторением первичного ключа. Приложения должны проектироваться таким образом, чтобы по возможности избегать конфликтов, поскольку реализация их обнаружения и разрешения приводит к повышению сложности, а также к издержкам при обработке и передаче данных. Чаще всего применяются следующие способы предотвращения конфликтов: обновление таблиц только на одном узле, а также фильтрация данных таким образом, чтобы отдельная строка обновлялась только одним узлом. В некоторых приложениях конфликты неизбежны. Например, в приложении для обработки продаж два продавца могут обслуживать одну территорию. Оба продавца могут обновлять данные для одного заказчика и заказов. Поэтому в поставщиках платформы Sync Framework предусмотрен набор функций, которые приложения могут использовать для обнаружения и разрешения конфликтов.

Конфликты данных могут произойти в любом сценарии синхронизации, предусматривающем обновление данных на нескольких узлах. Конфликты могут возникать не только при двунаправленной синхронизации, но и при синхронизации только с загрузкой или только на передачу. Например, если удалить строку на одном узле и обновить ту же строку на другом узле, то конфликт возникает, когда службы Sync Framework применяют обновление после передачи на сервер.

Конфликты всегда возникают между двумя узлами, на которых выполняется синхронизация. Рассмотрим следующий сценарий.

  1. Узел А и узел Б выполняют двунаправленную синхронизацию с узлом В.

  2. После обновления строки на узле А выполняется синхронизация. Конфликт не возникает, строка применяется на узле В.

  3. Та же строка обновлена на узле Б, после чего он выполняет синхронизацию. Строка, полученная от узла Б, конфликтует со строкой на сервере из-за обновления, созданного на узле А.

  4. Если принято решение о разрешении конфликта в пользу сервера, то службы Sync Framework применяют строку с сервера на узле Б. Если же принято решение о разрешении конфликта в пользу узла Б, то службы Sync Framework применят на сервере строку с узла Б. Во время последующей синхронизации между узлом А и сервером обновление, произведенное на узле Б, будете применено к узлу А.

Виды конфликтов и ошибок

Службы Sync Framework обнаруживают следующие виды конфликтов (они определены в перечислении DbConflictType).

  • Конфликт LocalInsertRemoteInsert возникает, если два узла вставляют строку с одинаковым первичным ключом. Его называют также конфликтом первичного ключа.

  • Конфликт LocalUpdateRemoteUpdate возникает в том случае, если два узла изменяют одну и ту же строку. Это наиболее распространенный тип конфликтов.

  • Конфликты LocalUpdateRemoteDelete и LocalDeleteRemoteUpdate возникают, если один узел обновляет строку, удаленную другим узлом.

  • Конфликт ErrorsOccurred возникает в том случае, если ошибка препятствует применению строки.

Обнаружение конфликтов и ошибок

Если строку не удалось применить во время синхронизации, то обычно причиной этого является ошибка или возникновение конфликта данных. В обоих случаях вызывается событие ApplyChangeFailed. Поставщик сообщает об ошибке узла, на котором обнаружен конфликт. Например, если задано значение UploadAndDownload для свойства Direction, то изменения вначале передаются с локального на удаленный поставщик. В этом случае поставщик, заданный для свойства RemoteProvider, вызывает ошибку. Если изменения вначале были загружены, а потом переданы, то ошибку вызывает поставщик, заданный для свойства LocalProvider. Независимо от того, какой поставщик создает событие и где находятся компоненты синхронизации, изменение данных на узле, создавшем данное событие, считается локальным изменением (LocalChange), а изменение другой строки — удаленным (RemoteChange). Это отличается от синхронизации «клиент-сервер», при которой объекты ClientChange и ServerChange всегда ассоциируются с клиентской базой данных и базой данных сервера соответственно.

После возникновения события ApplyChangeFailed выборка конфликтующих строк производится хранимой процедурой, создаваемой платформой Sync Framework для всех таблиц при предоставлении базы данных для синхронизации. По умолчанию этой процедуре присваивается имя <TableName>_selectrow. Платформа Sync Framework выполняет эту процедуру в том случае, если операция вставки, обновления или удаления возвратила в параметре @sync_row_count значение 0. Это значение указывает, что операция завершилась неуспешно.

Разрешение конфликтов и ошибок

Разрешение конфликтов и ошибок следует проводить в качестве ответной меры при возникновении события ApplyChangeFailed. Объект DbApplyChangeFailedEventArgs предоставляет доступ к нескольким свойствам, которые могут быть использованы в устранении конфликтов.

  • Укажите способ разрешения конфликтов. Для этого задайте одно из значений свойства Action, приведенных в перечислении ApplyAction.

    • Continue: не обрабатывать конфликт и продолжить синхронизацию.

    • RetryApplyingRow и RetryNextSync: повторить попытку применения строки. Если не устранить причину конфликта, внеся изменения в одну или обе конфликтующие строки, повторная попытка завершится неудачей и событие возникнет снова.

    • RetryWithForceWrite: повторить попытку с логикой принудительного применения изменений. При указании этого параметра значение переменной сеанса @sync_force_write задается равным 1. В подразделе «Примеры» этого раздела приведено описание принудительной перезаписи удаленным изменением локального изменения на основании логики в хранимой процедуре обновления, созданной платформой Sync Framework.

  • Получите тип конфликта и просмотрите конфликтующие строки с каждого узла с помощью свойства Conflict.

  • Получите набор данных изменений при синхронизации через свойство Context. Строки, полученные с помощью свойства Conflict, являются копиями. Поэтому их перезапись не изменяет применяемые строки. Доступный через свойство Context набор данных позволяет приложению при необходимости реализовать собственную схему разрешения конфликтов.

NoteПримечание.

API-интерфейс служб Sync Framework содержит один тип и одно свойство, связанные с устранением конфликтов, но не используемые в этой версии 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

См. также

Основные положения

Синхронизация SQL Server и SQL Server Compact