Windows Azure Table のトランザクションについて。

Azure テーブルは、端的には自由度が高いと言える。

その反面、SQLのDBテーブルのように、実質的なスキーマは無く、

また、テーブル間のリレーション(外部制約)も無いため、逆に扱いにくく感じる面もあるでしょう。



Windows Azure Storage Table で、知っておくべき重要な点は、

・一意性を確保するためのキー項目。PartitionKey、RowKey の2つの値で一意。

・レコードのエンティティ(項目)数の上限は、255個まで(キー項目も含む)。

・1レコードのデータ上限は、1Mバイトまで。

・レコードの更新には、Timestamp値による、楽観的同時実行制御が掛かる。


テーブルを利用するにあたっては、データ型も含めて、こんなところでしょう。


複数のテーブル間(ビジネス的な)のリレーション、およびトランザクションについて考えておくべき事項は、

・文字列型は、64KBまで。PartitionKey と RowKeyは文字列型という事。

・RowKey の値により、自動的にソートされる。

・1回のバッチ処理(複数レコードの一斉書込)は、100レコード且つ4Mバイトまで。

・バッチ処理では、Entity Group Transactions(EGT)のトランザクションが利用できる。

・EGTの範囲は、同一のPartitionKey、の同じテーブルに対してのみ。


という事を、常に考えながらテーブル設計をしないといけません。



まず、簡単な説明から

ここで、→ 前回の記事
Azure Storage Tableでオートナンバーを実現する採番テーブル

で、一部利用している所がある。

10レコードを追加した後に、
> context.SaveChanges(SaveChangesOptions.Batch);

を呼び出しています。

SaveChangesメソッドには、Batchオプションをつけて呼び出している。

これは、10レコードを一気に書き込みをしていますが、途中でエラーがあった場合には、

1行も書き込まれない事を意味します。

前回の記事での書き込み例では、RowKeyの重複は無いように作成するので、

途中でエラーが起こることは通常ありえないが、

もし偶然に、他のスレッドからRowKeyが重複するレコードが書き込まれた場合には、

一意制約でエラーとなり失敗します。


業務処理をする上では、RowKeyの重複が無い前提での処理を行う場合には、

偶然的にも、あり得ないように処理をするように心がけるべきです。



次に、インデックスの観点より、

Azure Table を使ったサンプルコードを見ると、

RowKeyの生成に、

DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks

というのを良く見かけます。

例:RowKey = string.Format("{0:10}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks) 
実際には、この後にも文字が続く..

Storage Table は、RowKey の値は、自動的にソートされます。というのを念頭に、

このようにすることで、新しいレコードは上位に来るという事になります。


具体的に言うと、例えば、テーブルに唯一のレコードが存在するとして、

RowKeyは、仮に、{"10文字_店舗コード5桁_店舗名"} と取り決めたとすると、

こんなデータになります。
"0000000001_00001_ショップ●●"


ショップコード"00001"のデータの取得は、

{RowKey抽出式 = SubString(11, 5).CompareTo("00001") >= 0 }.Take(1)

とすることで、最新のデータが取れます。

仮に、店舗登録が同時アクセスなどで、不整合なデータが出来たと仮定すると、

"0000000005_00001_ショップ■■"
"0000000004_00001_ショップ□□"
"0000000003_00001_ショップ△△"
"0000000002_00001_ショップ××"
"0000000001_00001_ショップ●●"

※先頭10文字は分かりやすいように記述してますが、実際は大きな数字です。

こんなデータが上から並んでいることになりますが、

最後に書き込まれたレコードは、"ショップ■■"として見なすことができます。

並び順は、上からなので効率の良い並び順となります。


これは、概念的な事を言ってますので、

たとえば、最新10件が業務に必要なデータとして、常に追加されるという様なケースでも、

利用できます。

溜まった不要データは、後でWorkerなどから削除すれば良いのです。



次に、トランザクションの話をします。

・EGTの範囲は、同一のPartitionKey、の同じテーブルに対してのみ。

という制約があります。

では、実際はどういう事なのかという例として、

2つのエンティティクラスを定義します。

論理的には、JOb(親) <-> Employee(子)のリレーションを考えたテーブルになります。

using System;
using System.Data.Services.Common;
namespace PubsStorageStore.TableEntitys
{
    [DataServiceKey("PartitionKey", "RowKey")]
    public class JobsEntity
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public DateTime Timestamp { get; set; }

        public string job_desc { get; set; }
        public int min_lvl { get; set; }
        public int max_lvl { get; set; }
    }

    [DataServiceKey("PartitionKey", "RowKey")]
    public class EmployeeEntity
    {
        public string PartitionKey { get; set; }
        public string RowKey { get; set; }
        public DateTime Timestamp { get; set; }

        public string fname { get; set; }
        public string minit { get; set; }
        public string lname { get; set; }
        public int job_id { get; set; }
        public int job_lvl { get; set; }
        public string pub_id { get; set; }
        public DateTime hire_date { get; set; }
    }
}

RowKeyの定義は、
{"JobIDとして10桁_EmployeeIDとして10桁"}
とします。

JObのレコードを作成するには、
JobsEntity.PartitionKey = "test-pubs1";
JobsEntity.RowKey = experienceDac.CreateNumber2("Job").ToString().PadLeft(10, '0');
..その他データ

Employeeのレコードを作成するには、
employeeEntity.PartitionKey = "test-pubs1";
employeeEntity.RowKey = JobsEntity.RowKey + "_" + experienceDac.CreateNumber2("Employee").ToString().PadLeft(10, '0');
..その他データ

※ID生成には、前回の採番処理を使っています。

1つのJobのAdd
context.AddObject("Employee", JobsEntity);

for 等で、複数のEmployeeのAdd
context.AddObject("Employee", employeeEntity);

書き込み
context.SaveChanges(SaveChangesOptions.Batch);

これにより、

PartitionKey RowKey job_desc min_lvl ... fname minit ...
test-pubs1 0000000001 入力値 入力値 ...      
test-pubs1 0000000001_0000000001       入力値 入力値 ...
test-pubs1 0000000001_0000000002       入力値 入力値 ...
test-pubs1 0000000001_0000000003       入力値 入力値 ...
test-pubs1 0000000001_0000000004       入力値 入力値 ...


というようなレコードが出来上がります。

具体的には、RowKeyはもっと考慮すべき事になると思いますが・・。

AddObjectでは、両方の書き込み対象として、"Employee"に書き込まれていることで、

EGTを利用することができます。



SaveChangesのバッチ動作は、中途にエラーがあれば、全て無効ということになります。

アプリケーションは、FULL REST設計でなければ、エンティティクラスを用いるでしょうから、

物理的に、Azure Table上の同一テーブルかどうかは、意識しないはずです。



関根: