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 には以下略
- 今回はデータアクセスに 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]); } } } }
まとめ
ひとつのメソッドした方が分かりやすいかなと思ったんですが、、、大分あれですね。失敗した><
簡単にまとめると以下のような感じですかね。
- インフラやミドルウェアは別途準備する必要がある
- マッピングの分割やシャードの作成はできるが、データの移動は出来ない
- 移動は、Split-Merge Tool を使うか自分で頑張るか
ShardMapManager
かRangeMapping
叩いてればなんとかなる- MultiShardQuery はまさかの
DataReader
ぐるぐる - 非同期 API はあんま見当たらない
API は大して多くもないですし、割と分かりやすいです。ドキュメント見なくても Intellisence で結構なんとかなります。ただ、毎回こんなに書いてられないのでラッパー欲しいですね。GA されたら誰か作ってくれるかな。