共用方式為


HOW TO:處理共同作業同步處理的資料衝突和錯誤 (SQL Server)

本主題為您示範如何在使用 Sync Framework 來同步處理 SQL Server 和 SQL Server Compact 資料庫時,處理資料衝突和錯誤。此主題中的範例將重點放在下列 Sync Framework 型別與事件:

如需如何執行範例程式碼的詳細資訊,請參閱同步處理 SQL Server 和 SQL Server Compact 中的<HOW-TO 主題中的範例應用程式>。

了解資料衝突和錯誤

在 Sync Framework 資料庫提供者中,系統會在資料列層級偵測衝突和錯誤。如果在同步處理之間有一個以上的節點中變更資料列,就會產生衝突。同步處理期間發生的錯誤基本上與條件約束違規有關,例如重複的主索引鍵。應用程式應該做盡量能夠避免衝突的設計,因為偵測與解決衝突會增加額外的複雜性、處理程序以及網路流量。最常見之避免衝突的方法如下:只在一個節點中更新資料表,或是對資料進行篩選,讓一個節點只更新一個特定的資料列。在某些應用程式中,衝突是無法避免的。例如,在銷售人力應用程式中,兩名銷售人員可能會有共同的區域。這兩名銷售人員都可以更新相同客戶和訂單的資料。因此,Sync Framework 提供一組功能,讓應用程式能夠用來偵測並解決衝突。

當在超過一個以上的節點中對同步處理案例做變更時,就有可能發生資料衝突。衝突可能發生在雙向 (Bidirectional) 同步處理當中,不過也可能在僅限下載或僅限上傳的同步處理中發生。例如,如果在一個節點刪除了資料列,而在另一個節點更新該資料列,當 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 列舉加以定義。

衝突和錯誤偵測

如果在同步處理中無法套用某個資料列,通常是因為發生錯誤或資料衝突。在這兩種情況下,都會引發 ApplyChangeFailed 事件。提供者會為偵測到衝突的節點引發錯誤。例如,如果您指定 Direction 屬性的值為 UploadAndDownload,變更會先從本機提供者上傳到遠端提供者。在此情況下,會由您為 RemoteProvider 屬性指定的提供者引發事件。如果變更是先下載,然後再上傳,則會由您為 LocalProvider 屬性指定的提供者引發事件。不管是哪個提供者引發事件,也不管同步處理元件的所在位置,引發事件之節點上的資料變更會視為本機變更 (LocalChange),而另一個資料列則視為遠端變更 (RemoteChange)。這和用戶端與伺服器同步處理不同,在後者之中 ClientChangeServerChange 永遠都是分別與用戶端資料庫及伺服器資料庫相關聯。

引發了 ApplyChangeFailed 事件之後,衝突的資料列是由 Sync Framework 在已佈建同步處理的資料庫時針對每個資料表建立的預存程序所選取。根據預設,這個程序名為 <TableName>_selectrow。當插入、更新或刪除作業所傳回的 @sync_row_count 值為 0 時,Sync Framework 就會執行這個程序。這個值表示作業失敗。

衝突和錯誤的解決方式

衝突和錯誤的解決應該以回應 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 事件處理常式中處理更新-更新衝突式。在範例中,衝突的資料列是顯示在主控台上,並有選項可指定衝突中哪個資料列應該優先處理。如果您執行本主題結尾的完整程式碼範例,將會看到兩組衝突的資料列:node 1 與 node 2 同步處理時,以及 node 2 與 node 3 同步處理時。

if (e.Conflict.Type == DbConflictType.LocalUpdateRemoteUpdate)
{

    //Get the conflicting changes from the Conflict object
    //and display them. The Conflict object holds a copy
    //of the changes; updates to this object will not be 
    //applied. To make changes, use the Context object.
    DataTable conflictingRemoteChange = e.Conflict.RemoteChange;
    DataTable conflictingLocalChange = e.Conflict.LocalChange;
    int remoteColumnCount = conflictingRemoteChange.Columns.Count;
    int localColumnCount = conflictingLocalChange.Columns.Count;

    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine("Row from database " + DbConflictDetected);
    Console.Write(" | ");

    //Display the local row. As mentioned above, this is the row
    //from the database at which the conflict was detected.
    for (int i = 0; i < localColumnCount; i++)
    {
        Console.Write(conflictingLocalChange.Rows[0][i] + " | ");
    }

    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine("Row from database " + DbOther);
    Console.Write(" | ");

    //Display the remote row.
    for (int i = 0; i < remoteColumnCount; i++)
    {
        Console.Write(conflictingRemoteChange.Rows[0][i] + " | ");
    }

    //Ask for a conflict resolution option.
    Console.WriteLine(String.Empty);
    Console.WriteLine(String.Empty);
    Console.WriteLine("Enter a resolution option for this conflict:");
    Console.WriteLine("A = change from " + DbConflictDetected + " wins.");
    Console.WriteLine("B = change from " + DbOther + " wins.");

    string conflictResolution = Console.ReadLine();
    conflictResolution.ToUpper();

    if (conflictResolution == "A")
    {
        e.Action = ApplyAction.Continue;
    }

    else if (conflictResolution == "B")
    {
        e.Action = ApplyAction.RetryWithForceWrite;
    }

    else
    {
        Console.WriteLine(String.Empty);
        Console.WriteLine("Not a valid resolution option.");
    }
}
If e.Conflict.Type = DbConflictType.LocalUpdateRemoteUpdate Then

    'Get the conflicting changes from the Conflict object 
    'and display them. The Conflict object holds a copy 
    'of the changes; updates to this object will not be 
    'applied. To make changes, use the Context object. 
    Dim conflictingRemoteChange As DataTable = e.Conflict.RemoteChange
    Dim conflictingLocalChange As DataTable = e.Conflict.LocalChange
    Dim remoteColumnCount As Integer = conflictingRemoteChange.Columns.Count
    Dim localColumnCount As Integer = conflictingLocalChange.Columns.Count

    Console.WriteLine([String].Empty)
    Console.WriteLine([String].Empty)
    Console.WriteLine("Row from database " & DbConflictDetected)
    Console.Write(" | ")

    'Display the local row. As mentioned above, this is the row 
    'from the database at which the conflict was detected. 
    For i As Integer = 0 To localColumnCount - 1
    Console.Write(conflictingLocalChange.Rows(0)(i).ToString() & " | ")
    Next

    Console.WriteLine([String].Empty)
    Console.WriteLine([String].Empty)
    Console.WriteLine([String].Empty)
    Console.WriteLine("Row from database " & DbOther)
    Console.Write(" | ")

    'Display the remote row. 
    For i As Integer = 0 To remoteColumnCount - 1
    Console.Write(conflictingRemoteChange.Rows(0)(i).ToString() & " | ")
    Next

    'Ask for a conflict resolution option. 
    Console.WriteLine([String].Empty)
    Console.WriteLine([String].Empty)
    Console.WriteLine("Enter a resolution option for this conflict:")
    Console.WriteLine("A = change from " & DbConflictDetected & " wins.")
    Console.WriteLine("B = change from " & DbOther & " wins.")

    Dim conflictResolution As String = Console.ReadLine()
    conflictResolution.ToUpper()

    If conflictResolution = "A" Then
    e.Action = ApplyAction.Continue

    ElseIf conflictResolution = "B" Then
        e.Action = ApplyAction.RetryWithForceWrite
    Else

        Console.WriteLine([String].Empty)
        Console.WriteLine("Not a valid resolution option.")
    End If

下列程式碼範例將錯誤資訊記錄到檔案。

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 類別,詳情請參閱資料庫提供者公用程式類別的 HOW-TO 主題

// 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