다음을 통해 공유


방법: 공동 작업 동기화의 데이터 충돌 및 오류 처리(SQL Server)

이 항목에서는 Sync Framework를 사용하여 SQL Server 및 SQL Server Compact 데이터베이스를 동기화할 때 데이터 충돌 및 오류를 처리하는 방법을 보여 줍니다. 이 항목의 예제에서는 다음과 같은 Sync Framework 형식 및 이벤트를 중점적으로 설명합니다.

샘플 코드를 실행하는 방법에 대한 자세한 내용은 SQL Server와 SQL Server Compact 동기화의 "방법 항목의 예제 응용 프로그램"을 참조하십시오.

데이터 충돌 및 오류 이해

Sync Framework 데이터베이스 공급자에서 충돌과 오류는 행 수준에서 검색됩니다. 충돌하는 행은 동기화 간에 둘 이상의 노드에서 변경된 행입니다. 동기화 중에 발생하는 오류는 일반적으로 중복 기본 키와 같은 제약 조건 위반과 관련되어 있습니다. 충돌을 검색하고 해결하려면 보다 정교한 기술과 처리 능력이 필요하고 네트워크 트래픽이 증가하므로 응용 프로그램은 가능한 한 충돌을 방지하도록 디자인되어야 합니다. 대개 한 노드에서만 테이블을 업데이트하거나, 특정 행이 한 노드에서만 업데이트되도록 데이터를 필터링하는 방식으로 충돌을 방지합니다. 일부 응용 프로그램에서는 충돌이 불가피하게 발생합니다. 예를 들어 영업 응용 프로그램에서 두 영업 사원이 지역을 공유할 수 있으며 동일한 고객과 주문에 대한 데이터를 업데이트할 수 있습니다. 따라서 Sync Framework는 응용 프로그램에서 충돌을 검색 및 해결하는 데 사용할 수 있는 여러 가지 기능을 제공합니다.

데이터 충돌은 둘 이상의 노드에서 변경 작업이 수행되는 동기화 작업에서 발생할 수 있습니다. 충돌은 양방향 동기화뿐 아니라 다운로드 전용 및 업로드 전용 동기화에서도 발생할 수 있습니다. 예를 들어 한 노드에서 삭제된 행이 다른 노드에서 업데이트된 경우 Sync Framework가 첫 번째 노드에서 업데이트를 업로드 및 적용하려고 하면 충돌이 발생합니다.

충돌은 항상 현재 동기화 중인 두 노드 사이에서 발생합니다. 다음과 같은 시나리오를 참조하십시오.

  1. 노드 A와 노드 B가 모두 노드 C와 양방향 동기화를 수행합니다.

  2. 노드 A에서 행을 업데이트한 다음 노드 A를 동기화합니다. 충돌이 발생하지 않으며 행이 노드 C에 적용됩니다.

  3. 같은 행을 노드 B에서 업데이트 한 다음 노드 B를 동기화합니다. 그러면 노드 A에서 수행된 업데이트로 인해 노드 B의 행이 노드 C의 행과 충돌 상태가 됩니다.

  4. 노드 C를 기준으로 이 충돌을 해결하려는 경우 Sync Framework에서 노드 C의 행을 노드 B에 적용할 수 있습니다. 노드 B를 기준으로 해결하려는 경우에는 Sync Framework에서 노드 B의 행을 노드 C에 적용할 수 있습니다. 이후에 노드 A와 노드 C를 동기화하면 노드 B의 업데이트가 노드 A에 적용됩니다.

충돌 및 오류의 유형

Sync Framework에서는 다음과 같은 유형의 충돌을 검색합니다. 이러한 충돌 유형은 DbConflictType 열거형에서 정의됩니다.

  • LocalInsertRemoteInsert 충돌은 두 노드에서 기본 키가 동일한 행을 삽입하는 경우에 발생합니다. 이러한 유형의 충돌을 기본 키 충돌이라고도 합니다.

  • LocalUpdateRemoteUpdate 충돌은 두 노드가 같은 행을 변경하는 경우에 발생합니다. 이는 가장 일반적인 충돌 유형입니다.

  • LocalUpdateRemoteDeleteLocalDeleteRemoteUpdate 충돌은 한 노드에서 삭제한 행을 다른 노드에서 업데이트하는 경우에 발생합니다.

  • ErrorsOccurred 충돌은 오류 때문에 행이 적용되지 못하는 경우에 발생합니다.

충돌 및 오류 검색

동기화 중에 행을 적용할 수 없으면 일반적으로 오류나 데이터 충돌이 발생했기 때문입니다. 두 경우 모두 ApplyChangeFailed 이벤트가 발생합니다. 공급자는 충돌이 검색된 노드에 대한 오류를 발생시킵니다. 예를 들어 Direction 속성에 UploadAndDownload 값을 지정하면 변경 내용이 우선 로컬 공급자에서 원격 공급자로 업로드됩니다. 이러한 경우 RemoteProvider 속성에 지정된 공급자가 이벤트를 발생시킵니다. 변경 내용을 우선 다운로드한 다음 업로드한 경우에는 LocalProvider 속성에 지정된 공급자가 이벤트를 발생시킵니다. 이벤트를 발생시킨 공급자와 동기화 구성 요소의 위치에 관계없이 이벤트가 발생한 노드의 데이터 변경 내용이 로컬 변경 내용(LocalChange)으로, 다른 행이 원격 변경 내용(RemoteChange)으로 간주됩니다. 이는 ClientChangeServerChange가 항상 클라이언트 데이터베이스와 서버 데이터베이스에 각각 연결되어 있는 클라이언트 및 서버 동기화와 다릅니다.

ApplyChangeFailed 이벤트가 발생한 후, 동기화에 대해 데이터베이스가 프로비전될 때 Sync Framework에서 각 테이블에 대해 만드는 저장 프로시저에 의해 충돌하는 행이 선택됩니다. 기본적으로 이 프로시저의 이름은 <TableName>_selectrow입니다. Sync Framework에서는 삽입, 업데이트 또는 삭제 작업에서 @sync_row_count 값이 0으로 반환되면 이 프로시저를 실행합니다. 이 값은 작업이 실패했음을 나타냅니다.

충돌 및 오류 해결

충돌 및 오류 해결은 ApplyChangeFailed 이벤트에 대한 응답으로 처리해야 합니다. DbApplyChangeFailedEventArgs 개체를 통해 충돌 해결 중에 사용할 수 있는 다양한 속성에 액세스할 수 있습니다.

  • Action 속성을 ApplyAction 열거형의 값 중 하나로 설정하여 충돌을 해결하는 방법을 지정합니다.

    • Continue: 충돌을 무시하고 동기화를 계속합니다.

    • RetryApplyingRowRetryNextSync: 행 적용을 다시 시도합니다. 충돌하는 행 중 하나 또는 두 행을 모두 변경하여 충돌의 원인을 해결하지 않으면 행 다시 적용이 실패하며 이벤트가 다시 발생합니다.

    • RetryWithForceWrite: 논리로 다시 시도하여 변경 내용을 강제로 적용합니다. 이 옵션을 지정하면 세션 변수 @sync_force_write가 1로 설정됩니다. 이 항목의 "예제" 섹션에서는 Sync Framework가 만든 업데이트 저장 프로시저의 논리를 기준으로 로컬 변경 내용을 강제로 원격 변경 내용으로 덮어쓰는 방법을 보여 줍니다.

  • Conflict 속성을 사용하여 충돌 유형을 가져오고 각 노드에서 충돌하는 행을 살펴봅니다.

  • Context 속성을 사용하여 동기화 중인 변경 내용의 데이터 집합을 가져옵니다. Conflict 속성에서 노출하는 행은 복사본입니다. 따라서 이러한 행을 덮어써도 적용되는 행은 변경되지 않습니다. 사용자 지정 해결 체계가 응용 프로그램에 필요한 경우 Context 속성이 노출하는 데이터 집합을 사용하여 사용자 지정 해결 체계를 개발합니다.

참고

Sync Framework API에는 이 버전의 API에서 사용되지 않지만 충돌 해결과 관련되어 있는 형식과 속성인 DbResolveActionConflictResolutionPolicy가 포함되어 있습니다.

예제

다음 코드 예제에서는 충돌 검색 및 해결을 구성하는 방법을 보여 줍니다.

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 클래스가 필요합니다.

// 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 동기화