かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Sync Framework 2.1による2DBの同期

Sync Frameworkって個人的に地味だと思ってたのですが、使ってみると意外と気軽にDBの同期ができたのでびっくりしました。SQL Azure同士の同期もばっちり。ということで、SQL Server(Azure)同士の同期をとるための部品をこしらえてみました。ローカルのSQL ServerのExpress Editionに、SyncSourceDatabaseというDBとSyncTargetDatabaseというDBを作ってSyncSourceDatabaseに以下のようなテーブルを作ります。

create table Departments (
  id int IDENTITY(1,1) primary key,
  name nvarchar(255) not null
);
create table Employees (
  id int IDENTITY(1,1) primary key,
  name nvarchar(255) not null,
  DepartmentId int not null,
  constraint FK_Employees_Departments foreign key(DepartmentId) references Departments(id)
);

上記のSyncTargetDatabaseというDBの2テーブルとSyncTargetDatabaseを同期するためのコードは以下のような感じになります。プロジェクトには以下のdllを参照しておいてください。

  • Microsoft.Synchronization.dll
  • Microsoft.Synchronization.Data.dll
  • Microsoft.Synchronization.Data.SqlServer.dll
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using Microsoft.Synchronization;
using Microsoft.Synchronization.Data;
using Microsoft.Synchronization.Data.SqlServer;

namespace SyncClientSample
{
    class Program
    {
        private static readonly string SourceConnectionString = @"Data Source=.\SQLEXPRESS;Initial Catalog=SyncSourceDatabase;Integrated Security=True;Pooling=False";
        private static readonly string TargetConnectionString = @"Data Source=.\SQLEXPRESS;Initial Catalog=SyncTargetDatabase;Integrated Security=True;Pooling=False";
        static void Main(string[] args)
        {
            var sync = new SyncFrameworkUtil(
                SourceConnectionString,
                TargetConnectionString,
                "sampleScope",
                // 外部キーつきのテーブルを指定するには、SQLで作成する順番通りに
                // 指定しないとエラーになる。(DepartmentsとEmployeesを入れ替えたら外部キー定義時にエラーになる)
                new SyncTableInfo(
                    "Departments"),
                new SyncTableInfo(
                    "Employees",
                    new DbSyncForeignKeyConstraint(
                        "FK_Employees_Departments",
                        "Departments",
                        "Employees",
                        "Id",
                        "DepartmentId")));
            // 初期化実行
            sync.Setup();

            // 同期
            var r = sync.Sync(SyncDirectionOrder.DownloadAndUpload);

            // 結果表示
            Report(r);
        }

        private static void Report(SyncOperationStatistics result)
        {
            Console.WriteLine("-Time report");
            Console.WriteLine("  SyncStartTime: {0}", result.SyncStartTime);
            Console.WriteLine("  SyncEndTime: {0}", result.SyncEndTime);
            Console.WriteLine("  TotalTime: {0}ms", (result.SyncEndTime - result.SyncStartTime).Milliseconds);
            Console.WriteLine("-Upload report");
            Console.WriteLine("  UploadChangesTotal: {0}", result.UploadChangesTotal);
            Console.WriteLine("  UploadChangesApplied: {0}", result.UploadChangesApplied);
            Console.WriteLine("  UploadChangesFailed: {0}", result.UploadChangesFailed);
            Console.WriteLine("-Download report");
            Console.WriteLine("  DownloadChangesTotal: {0}", result.DownloadChangesTotal);
            Console.WriteLine("  DownloadChangesApplied: {0}", result.DownloadChangesApplied);
            Console.WriteLine("  DownloadChangesFailed: {0}", result.DownloadChangesFailed);
        }
    }
    
    /// <summary>
    /// SQL Server to SQL Serverの同期に特化してAPIを簡略化
    /// </summary>
    public class SyncFrameworkUtil {
        /// <summary>
        /// 同期元の接続文字列
        /// </summary>
        private readonly string SourceConnectionString;

        /// <summary>
        /// 同期先の接続文字列
        /// </summary>
        private readonly string TargetConnectionString;

        /// <summary>
        /// 同期のスコープ名
        /// </summary>
        private readonly string ScopeName;

        /// <summary>
        /// 同期対象のテーブル名(と外部キー)
        /// </summary>
        private readonly IEnumerable<SyncTableInfo> Tables;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="sourceConnectionString">同期元の接続文字列</param>
        /// <param name="targetConnectionString">同期先の接続文字列</param>
        /// <param name="scopeName">同期のスコープ名</param>
        /// <param name="tables">同期するテーブルの情報</param>
        public SyncFrameworkUtil(
            string sourceConnectionString, 
            string targetConnectionString, 
            string scopeName,
            params SyncTableInfo[] tables)
	{
            this.SourceConnectionString = sourceConnectionString;
            this.TargetConnectionString = targetConnectionString;
            this.ScopeName = scopeName;
            this.Tables = tables;
	}

        /// <summary>
        /// 同期のためのセットアップ処理(大量にトリガーとストアドとテーブルが作られる…)
        /// </summary>
        public void Setup()
        {
            using (var targetConnection = new SqlConnection(this.TargetConnectionString))
            using (var sourceConnection = new SqlConnection(this.SourceConnectionString))
            {
                InitializeSyncScopeProvisioning(sourceConnection, targetConnection);
            }
        }

        private void InitializeSyncScopeProvisioning(SqlConnection sourceConnection, SqlConnection targetConnection)
        {
            var scope = new DbSyncScopeDescription(ScopeName);
            var syncDescriptions = this.Tables
                .Select(t =>
                {
                    var d = SqlSyncDescriptionBuilder.GetDescriptionForTable(t.Name, sourceConnection);
                    foreach (var fk in t.ForeignKeys)
                    {
                        d.Constraints.Add(fk);
                    }
                    return d;
                });

            foreach (var d in syncDescriptions)
            {
                scope.Tables.Add(d);
            }

            var sourceSqlServerProvisioning = new SqlSyncScopeProvisioning(sourceConnection, scope);
            if (!sourceSqlServerProvisioning.ScopeExists(ScopeName))
            {
                sourceSqlServerProvisioning.Apply();
            }

            var destSqlServerProvisioning = new SqlSyncScopeProvisioning(targetConnection, scope);
            if (!destSqlServerProvisioning.ScopeExists(ScopeName))
            {
                destSqlServerProvisioning.Apply();
            }
        }

        /// <summary>
        /// 同期の実行
        /// </summary>
        /// <param name="directionOrder">同期の向きを指定</param>
        /// <returns>同期の結果</returns>
        public SyncOperationStatistics Sync(SyncDirectionOrder directionOrder)
        {
            using (var targetConnection = new SqlConnection(TargetConnectionString))
            using (var sourceConnection = new SqlConnection(SourceConnectionString))
            {
                return SynchronizedImpl(sourceConnection, targetConnection, directionOrder);
            }
        }

        private SyncOperationStatistics SynchronizedImpl(SqlConnection sourceConnection, SqlConnection targetConnection, SyncDirectionOrder directionOrder)
        {
            var orchestrator = new SyncOrchestrator
            {
                LocalProvider = new SqlSyncProvider(ScopeName, targetConnection),
                RemoteProvider = new SqlSyncProvider(ScopeName, sourceConnection),
                Direction = directionOrder
            };
            return orchestrator.Synchronize();
        }

        
    }

    /// <summary>
    /// 同期するテーブルの情報
    /// </summary>
    public class SyncTableInfo
    {
        /// <summary>
        /// テーブル名
        /// </summary>
        public string Name { get; private set; }
        
        /// <summary>
        /// 対象の外部キー
        /// </summary>
        public IEnumerable<DbSyncForeignKeyConstraint> ForeignKeys { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="name">テーブル名</param>
        /// <param name="foreignKeys">対象の外部キー(名前だけの指定だと初期化時にエラーになるため親テーブル、子テーブル、カラム名</param>
        public SyncTableInfo(string name, params DbSyncForeignKeyConstraint[] foreignKeys)
        {
            this.Name = name;
            this.ForeignKeys = foreignKeys;
        }
    }
}

SyncFrameworkUtilクラスが一応SyncFrameworkでSQL Server同士の同期をとるために特化したクラスです。Setupメソッドで同期用のテーブルのセットアップを行い、Syncメソッドで実際に同期処理を行います。これだけで差分のみの同期ができるって素敵。

ランタイムが必要だったりする

このSyncFrameworkはランタイムが必要だったりします。SyncFramework 2.1のSDKを入れれば入りますが、ランタイムだけのインストーラも配布されています。

あとは、Windows AzureのWebロールやWorkerロールに入れるための手順も下記のページで紹介されています。COMのコンポーネントをプライベートに配置する方法があるなんて知りませんでした。この処理をApplication_Startイベントあたりで呼んでやればいいと思います。

これならSQL Azure Data Syncを使わなくても、結構手軽に同期処理をかけるやもしれん。因みに、SQL Server CE 4.0あたりとも同期できるだろうから、これがうまいことはまるシステムだとオンラインになったら同期とかっていうアプリも意外と簡単に作れそうだ。