Azure SQL Database Elastic Scale Client Library 使い方簡易まとめ

Connect(); の発表でテンション上がってきましたね。追うべき情報がたくさんあって大変ですが楽しいです。MS かっけーぜ。

ただその前に、やりかけだった Azure SQL Database Elastic Scale Client Library の基本的な使い方を備忘用にまとめておこうと思います。

基本的には

Azure SQL Database Elastic Scale

からたどれる情報をまとめただけですが、ページが分散してるといざ使うときに面倒ですしね。

前提

  • ライブラリのバージョンは 0.7.0 です
  • Azure SQL Server および SQL Database は事前に作成しておきます
    • Azure SQL Database Elastic Scale Client Library にはそれらを作れる API はありません
    • 管理用 DB は "shard_manager"、シャード DB は "shard_1"、"shard_2" という名前にしています
  • テーブル定義も作成しておきます
    • Azure SQL Database Elastic Scale Client Library には以下略
  • 今回はデータアクセスに Entity Framework を使います
    • コードファースト
    • 普段使わないので何か間違ってるかもしれませんがスルーしてください><

実装

では早速。まずは Entity の定義から。

public class Sales
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int SalesId { get; set; }

    [Required]
    public int ItemId { get; set; }

    [Required]
    public Item Item { get; set; }

    public override string ToString()
    {
        return string.Format("SalesId:{0}, ItemId:{1}, Item:{{{2}}}",
            this.SalesId, this.ItemId, this.Item);
    }
}

public class Item
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int ItemId { get; set; }

    [Required]
    public int UnitPrice { get; set; }

    public override string ToString()
    {
        return string.Format("ItemId:{0}, UnitPrice:{1}", this.ItemId, this.UnitPrice);
    }
}

続いて実装です。かなり長くなってしまいました。。。

static readonly SqlConnectionStringBuilder BaseConn = new SqlConnectionStringBuilder
{
    DataSource = "<your server>",
    UserID = "<your id>",
    Password = "<your password>",
    TrustServerCertificate = false,
    Encrypt = true,
    ConnectTimeout = 30
};

static void Main(string[] args)
{
    SetupAll();

    var mapName = "your_map_name";
    var managerConnStr = new SqlConnectionStringBuilder(BaseConn.ToString()) { InitialCatalog = "shard_manager" };

    // ■ ShardMap の削除
    ShardMapManager manager;
    if (ShardMapManagerFactory.TryGetSqlShardMapManager(
        managerConnStr.ToString(), ShardMapManagerLoadPolicy.Lazy, out manager))
    {
        RangeShardMap<int> shardMap = null;
        if (manager.TryGetRangeShardMap(mapName, out shardMap))
        {
            // 全てのマッピングの削除
            foreach (var mapping in shardMap.GetMappings().OrderBy(_ => _.Value.Low))
            {
                shardMap.MarkMappingOffline(mapping);
            }

            // Offline と Delete は分けないと例外スロー
            foreach (var mapping in shardMap.GetMappings().OrderBy(_ => _.Value.Low))
            {
                shardMap.DeleteMapping(mapping);
            }

            // Shard のスキーマ情報削除
            var schemas = manager.GetSchemaInfoCollection();
            if(schemas.Any())
            {
                schemas.Remove(mapName);
            }

            // 全てのシャードの削除
            foreach (var shard in shardMap.GetShards())
            {
                shardMap.DeleteShard(shard);
            }

            manager.DeleteShardMap(shardMap);
        }
    }

    // ■ ShardMapManager の取得 or 作成
    //   ShardMapManager とは、Shard 管理用 DB およびそこに属する管理用テーブル(__ShardManagement.ほげほげGlobal)を指す
    if (!ShardMapManagerFactory.TryGetSqlShardMapManager(
        managerConnStr.ToString(),
        ShardMapManagerLoadPolicy.Lazy,
        RetryBehavior.DefaultRetryBehavior, // Default は何もせずに false を返してるのでリトライしない
        out manager))
    {
        manager = ShardMapManagerFactory.CreateSqlShardMapManager(
            managerConnStr.ToString(),
            ShardMapManagerCreateMode.KeepExisting, // KeepExisting => 既に存在したら例外、ReplaceExisting => 上書き
            RetryBehavior.DefaultRetryBehavior);
    }

    // ■ RangeShardMap の取得 or 作成
    //   RangeShardMap とはマッピングの定義(のひとつ)。
    //   ShardMapManager Shard の ShardMapsGlobal テーブルに
    //   「名前は "mapName"、マッピングの種類は 2(=Range)、キータイプは 1(=int32)」といったメタデータを作る
    RangeShardMap<int> rangeShardMap = null;
    if (!manager.TryGetRangeShardMap(mapName, out rangeShardMap))
    {
        rangeShardMap = manager.CreateRangeShardMap<int>(mapName);
    }
        

    // ■ SchemaInfo の作成
    //   SchemaTableInfo とは、どのテーブルをどの列をキーに分散するのか定義した XML
    //   ReferenceTableInfo とは、分散はしないが分散してるテーブルが外部参照しているマスタテーブルを定義した XML
    var info = new SchemaInfo();
    info.Add(new ShardedTableInfo("Sales", "SalesId"));
    info.Add(new ReferenceTableInfo("Item"));
    manager.GetSchemaInfoCollection().Add(mapName, info);

    // ■ Shard の取得 or 作成
    //   Shard とは、バージョンや ShardMapId(= ↑ で作成したシャードマップの ID)、サーバー名や DB 名を持つ
    //   また、Shard 作成時には ShardLocation で指定した DB に管理用テーブル(__ShardManagement.ほげほげLocal) を作成する
    //   ShardMapsLocal テーブルや ShardsLocal テーブルには自身のシャードマップやシャード情報が格納される
    Shard shard1 = null;
    var location1 = new ShardLocation(BaseConn.DataSource, "shard_1");
    if (!rangeShardMap.TryGetShard(location1, out shard1))
    {
        shard1 = rangeShardMap.CreateShard(location1);
    }

    // ■ RangeMapping の作成
    //   RangeMapping とは、RangeShardMap で定められた定義をもとに作られた、
    //   具体的なマッピング(キーの範囲やそれらの格納場所、Online/Offline ステータス)を指す
    //   作成時には、ShardMapManager Shard と、指定した Shard にマッピングレコードを作成する
    RangeMapping<int> mapping1 = null;
    if (!rangeShardMap.TryGetMappingForKey(0, out mapping1))
    {
        mapping1 = rangeShardMap.CreateRangeMapping(
            new RangeMappingCreationInfo<int>(
                new Range<int>(0, 20), shard1, MappingStatus.Online)); // キー値 0~19
    }

    // *** ここから分散 ***

    // ■ 移動対象となる Shardlet を取得してキャプチャ。DB からは削除
    List<Sales> salesCapture;
    List<Item> itemCapture;
    var connStrWithoutServerAndDb = new SqlConnectionStringBuilder(BaseConn.ToString()) { DataSource = "" };
    using (var conn = rangeShardMap.OpenConnectionForKey(0, connStrWithoutServerAndDb.ToString()))
    using (var context = new MyDbContext(conn, contextOwnsConnection:true))
    {
        itemCapture = context.Item.ToList();
        salesCapture = context.Sales.Where(_ => _.SalesId >= 10).ToList();
        context.Sales.RemoveRange(salesCapture);
        context.SaveChanges();
    }

    // ■ 分割先の Shard を作成
    Shard shard2 = null;
    var location2 = new ShardLocation(BaseConn.DataSource, "shard_2");
    if (!rangeShardMap.TryGetShard(location2, out shard2))
    {
        shard2 = rangeShardMap.CreateShard(location2);
    }

    // ■ マッピングを分割
    var existMappings = rangeShardMap.GetMappings().ToArray();
    rangeShardMap.SplitMapping(existMappings.Single(), splitAt: 10); // splitAt は分割先マッピングの最小キー値

    // ■ 新しいマッピングを分割先の Shard に作る
    RangeMapping<int> mapping2;
    if (!rangeShardMap.TryGetMappingForKey(10, out mapping2))
    {
        rangeShardMap.CreateRangeMapping(
            new RangeMappingCreationInfo<int>(
                new Range<int>(10, 20), shard2, MappingStatus.Online));
    }

    var newMapping = rangeShardMap.UpdateMapping(mapping2, new RangeMappingUpdate
    {
        Shard = shard2,
        Status = MappingStatus.Offline // 作成と同時に Online にすると例外
    });
    rangeShardMap.MarkMappingOnline(newMapping); // マッピングの有効化

    // ■ 後半のマッピングに属するデータを登録
    using (var conn = rangeShardMap.OpenConnectionForKey(10, connStrWithoutServerAndDb.ToString()))
    using (var context = new MyDbContext(conn, contextOwnsConnection:true))
    {
        context.Sales.AddRange(
            salesCapture.Join(itemCapture, _ => _.ItemId, __ => __.ItemId, (s, i) =>
            {
                s.Item = i;
                return s;
            }));
        context.SaveChanges();
    }

    // *** 分散ここまで ***

    // *** Shardled の取得開始***

    // ■ 分割元の Shard から Shardlet を取得
    using (var conn = rangeShardMap.OpenConnectionForKey(0, connStrWithoutServerAndDb.ToString()))
    using (var context = new MyDbContext(conn, true))
    {
        context.Sales.Include("Item").ToList().ForEach(Console.WriteLine);
    }

    // ■ 分割先の Shard から Shardlet を取得
    using (var conn = rangeShardMap.OpenConnectionForKey(10, connStrWithoutServerAndDb.ToString()))
    using (var context = new MyDbContext(conn, true))
    {
        context.Sales.Include("Item").ToList().ForEach(Console.WriteLine);
    }

    // ■ 全ての Shard から Shardlet を取得(MultiShardQuery)
    // MultiShardQuery クラスが DbConnection を継承してないので地道に MultiShardReader をぐるぐる回すしかない?
    using (var conn = new MultiShardConnection(rangeShardMap.GetShards(), connStrWithoutServerAndDb.ToString()))
    using (var cmd = conn.CreateCommand())
    {
        cmd.CommandText = @"select x.SalesId, y.* from Sales x inner join Item y on x.ItemId = y.ItemId order by x.SalesId";

        // IncludeShardNameColumn => Shardlet が属する Shard 情報がセットされた新しい列を最後尾に付与する
        cmd.ExecutionOptions = MultiShardExecutionOptions.IncludeShardNameColumn;

        // PartialResults => 一部の Shard に接続できなくても、残りの Shard から得られた結果を返す
        // CompleteResults => 全ての Shard に接続できた時に、結果を返す
        cmd.ExecutionPolicy = MultiShardExecutionPolicy.PartialResults;

        cmd.CommandTimeout = 30;

        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                var values = new object[reader.FieldCount];
                reader.GetValues(values);
                var data = new Sales
                {
                    SalesId = (int)values[0],
                    ItemId = (int)values[1],
                    Item = new Item
                    {
                        ItemId = (int)values[1],
                        UnitPrice = (int)values[2]
                    }
                };
                Console.WriteLine("data => {0}, Shard => {1}", data, (string)values[3]);
            }
        }
    }
}

まとめ

ひとつのメソッドした方が分かりやすいかなと思ったんですが、、、大分あれですね。失敗した><

簡単にまとめると以下のような感じですかね。

  • インフラやミドルウェアは別途準備する必要がある
  • マッピングの分割やシャードの作成はできるが、データの移動は出来ない
  • ShardMapManagerRangeMapping 叩いてればなんとかなる
  • MultiShardQuery はまさかの DataReader ぐるぐる
  • 非同期 API はあんま見当たらない

API は大して多くもないですし、割と分かりやすいです。ドキュメント見なくても Intellisence で結構なんとかなります。ただ、毎回こんなに書いてられないのでラッパー欲しいですね。GA されたら誰か作ってくれるかな。