Share via


コラボレーションでの同期中に発生するデータ競合およびエラーを処理する方法 (SQL Server)

このトピックでは、Sync Framework を使用して SQL Server データベースと SQL Server Compact データベースを同期するときのデータ競合およびエラーを処理する方法について説明します。このトピックの例では、次に示す Sync Framework の型とイベントを中心に説明します。

サンプル コードを実行する方法の詳細については、「SQL Server と SQL Server Compact の同期」の「操作方法に関するトピックのサンプル アプリケーション」を参照してください。

データの競合とエラーについて

Sync Framework データベース プロバイダーでは、競合とエラーが行レベルで検出されます。行の競合は、前回の同期から今回の同期までに、複数のノードで変更があった場合に発生します。同期中に発生するエラーでは、通常、主キーの重複などの制約違反が原因として挙げられます。アプリケーションは、可能な限り競合を回避するようにデザインする必要があります。これは、競合検出と解決により、複雑さが増し、処理とネットワーク トラフィックが増加するためです。競合を回避する最も一般的な方法としては、テーブルの更新を 1 つのノードのみで行うか、同じ行の更新を 1 つのノードでしか行わないようにデータをフィルターで選択する方法があります。アプリケーションによっては、競合を避けられない場合もあります。たとえば、営業部門のアプリケーションでは、2 人の営業担当者が 1 つの営業区域を共有し、両方の営業担当者が同じ顧客および注文のデータを更新する可能性があります。そのため、Sync Framework には、アプリケーションが競合を検出および解決するために使用できる一連の機能が用意されています。

データの競合は、複数のノードでデータが変更されるすべての同期シナリオで発生する可能性があります。競合は双方向同期で発生しますが、ダウンロードのみの同期やアップロードのみの同期でも発生することがあります。たとえば、ある行があるノードで削除され、同じ行が別のノードで更新された場合、Sync Framework が更新を 1 つ目のノードにアップロードして適用しようとすると競合が発生します。

競合は、常に、現在同期中の 2 つのノードの間で発生します。以下のシナリオについて考えてみます。

  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 競合。2 つのノードで同じ主キーを持つ行が挿入されたときに発生します。この種類の競合は、主キーの衝突とも呼ばれます。

  • LocalUpdateRemoteUpdate 競合。2 つのノードで同じ行が変更されたときに発生します。これは、最も一般的な種類の競合です。

  • LocalUpdateRemoteDelete 競合および LocalDeleteRemoteUpdate 競合。一方のノードで削除された行がもう一方のノードで更新されたときに発生します。

  • ErrorsOccurred 競合。エラーが原因で行を適用できないときに発生します。

競合とエラーの検出

同期中に行を適用できない場合は、通常、エラーまたはデータの競合が発生したことがその原因です。どちらの場合も、ApplyChangeFailed イベントが発生します。プロバイダーは、競合が検出されたノードに対してエラーを発生させます。たとえば、Direction プロパティに値 UploadAndDownload を指定した場合、変更は、最初にローカル プロバイダーからリモート プロバイダーにアップロードされます。この場合、RemoteProvider プロパティに指定されたプロバイダーによってイベントが発生します。変更が最初にダウンロードされた後でアップロードされた場合は、LocalProvider プロパティに指定されたプロバイダーによってイベントが発生します。イベントを発生させたプロバイダーや同期コンポーネントの格納場所に関係なく、イベントが発生したノードでのデータ変更はローカルの変更 (LocalChange) と見なされ、もう一方の行はリモートの変更 (RemoteChange) と見なされます。これはクライアントとサーバーの同期とは異なります。クライアントとサーバーの同期では、ClientChangeServerChange がそれぞれクライアント データベースとサーバー データベースに常に関連付けられます。

データベースを同期用に準備したときに Sync Framework が個々のテーブルにストアド プロシージャを作成しますが、ApplyChangeFailed イベントが発生した後は、このストアド プロシージャによって、競合する行が選択されます。既定では、このプロシージャは <TableName>_selectrow と名付けられています。挿入、更新、または削除操作が @sync_row_count の値として 0 を返すと、Sync Framework によってこのプロシージャが実行されます。この値は、操作が失敗したことを示します。

競合とエラーの解決

競合とエラーの解決は、ApplyChangeFailed イベントに応答して処理する必要があります。DbApplyChangeFailedEventArgs オブジェクトを使用すると、競合解決時に使用できるいくつかのプロパティにアクセスできます。

  • Action プロパティを ApplyAction 列挙体の次の値のいずれかに設定することによって、競合解決の方法を指定できます。

    • Continue : 競合を無視して同期を続行します。

    • RetryApplyingRow および RetryNextSync : 行の適用を再試行します。競合する行の一方または両方を変更して競合の原因に対処しないと、再試行が失敗し、イベントが再度発生します。

    • RetryWithForceWrite : 変更の適用を強制するロジックで再試行します。このオプションを指定するとセッション変数 @sync_force_write が 1 に設定されます。このトピックの「例」セクションでは、Sync Framework が作成する更新ストアド プロシージャ内のロジックに基づいて、リモート変更がローカル変更を上書きするように強制する方法を示します。

  • Conflict プロパティを使用すると、競合の種類を取得し、各ノードの競合する行を表示できます。

  • Context プロパティを使用すると、同期中の変更のデータセットを取得できます。Conflict プロパティによって公開される行はコピーです。したがって、この行を上書きしても、適用される行は変更されません。アプリケーションで必要であれば、Context プロパティによって公開されるデータセットを使用してカスタムの解決方法を開発します。

注意

Sync Framework API には、競合の解決に関連する 1 つの型と 1 つのプロパティ (DbResolveAction および ConflictResolutionPolicy) が含まれていますが、このバージョンの API では使用されません。

次のコード例は競合の検出と解決を構成する方法を示します。

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 を同期したときの、2 つの競合する行のセットが示されます。

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 の同期