2011年11月28日

Azure Tableによるデータ検索処理

Windows AzureのTableストレージにおけるデータ検索では、基本的にはStorage Client LibraryのLinq to Azure Tableを使って開発するものの、AzureのLinqでは一部のLinq式しかサポートしておらず大部分のLinq式がサポート外になっています。http://msdn.microsoft.com/en-us/library/windowsazure/dd135725.aspx

そのため、Azure Tableにおいてデータ検索を行う際は、Linq to Objectを並行して利用する必要が出てきます。この使い分けについての基本的な考え方は表示対象エンティティをLinq to Azure Tableで取得し、その後のデータ加工処理をLinq to Objectで行うといった雰囲気です。ただしこの2つのLinqの使い分けでは、Linq to Azure Tableにおける取得可能なエンティティ数に制限がある(1000件未満、クエリ実行時間5秒以下 参考:http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx)ので、この制限にかからないよう十分にLinq to Azure Tableで絞込みを行う必要です。

Linq to Azure TableとLinq to Objectの違い

Linq to Azure TableとLinq to Objectを使い分けるにあたって、IQueryableとIEnumerableの違いについて十分に理解する必要があります。簡単に説明すると、IQueryableはクエリ式を保持しているもので、IEnumerableはデータ列を保持するものです。そのため、IQueryableに対するWhere条件の追加はクエリ式への条件追加、IEnumerableに対するWhere条件の追加はデータのフィルタリング条件の追加となります。

それぞれで実際にどのようなクエリが流れるのかは以下の通りです。

IQueryableに対するWhere条件の追加

以下のコードは、IQueryable(Linq to Azure Table)に対してRowKeyが2000以上というWhere条件を追加したもので、上位3件のみ取得しています。

   var q = from e in ctx.CreateQuery<Data>("Entities")
     where e.PartitionKey == "0" && e.RowKey.CompareTo("2000") > 0
     select e;

   var list = q.Take(3).ToList();

   Console.WriteLine("### Linq to Azure Table");
   foreach ( var e in list ) {
    Console.WriteLine("{0}: Name={1}", e.RowKey, e.Name);
   }
   Console.WriteLine("Count = {0}", list.Count);

これを実行すると以下のような出力が得られ、指定したエンティティが正しく取得できていることがわかります。

### Linq to Azure Table
2001: Name=Sample_2001
2002: Name=Sample_2002
2003: Name=Sample_2003
Count = 3

またこれを実行した際、以下のHTTPリクエストが投げられています。これを見ると、$filterおよび$topの条件としてコード上で指定した条件が正しく反映されていることがわかります。

GET http://127.0.0.1:10002/devstoreaccount1/Entities()?$filter=(PartitionKey%20eq%20'0')%20and%20(RowKey%20gt%20'2000')&$top=3 HTTP/1.1
User-Agent: Microsoft ADO.NET Data Services
DataServiceVersion: 1.0;NetFx
MaxDataServiceVersion: 2.0;NetFx
x-ms-version: 2011-08-18
x-ms-date: Mon, 28 Nov 2011 02:22:35 GMT
Authorization: SharedKeyLite devstoreaccount1:dnGajZtxeijsvqtmXUFIh+qFWgGVBsT2nhFnPPffXP8=
Accept: application/atom+xml,application/xml
Accept-Charset: UTF-8
Host: 127.0.0.1:10002

IEnumerableに対するWhere条件の追加

そしてもう一方のIEnumerable(Linq to Object)に対して上記と同様の条件を指定したものが以下のコードです。上記コードとの違いはIQueryableからIEnumerableに変換するために、IQueryable#AsEnumerableを呼び出している点のみです。

   var q = from e in ctx.CreateQuery<Data>("Entities").AsEnumerable()
      where e.PartitionKey == "0" && e.RowKey.CompareTo("2000") > 0
      select e;

   var list = q.Take(3).ToList();

   Console.WriteLine("### Linq to Object");
   foreach ( var e in list ) {
    Console.WriteLine("{0}: Name={1}", e.RowKey, e.Name);
   }
   Console.WriteLine("Count = {0}", list.Count);

   Console.ReadLine();

このコードを実行すると以下の出力が得られますが、取得データ件数が0件となってしまいました。

### Linq to Object
Count = 0
そしてこの時のHTTPリクエストの内容を見ると、$filterや$topの条件が指定されていないことがわかります。
GET http://127.0.0.1:10002/devstoreaccount1/Entities HTTP/1.1
User-Agent: Microsoft ADO.NET Data Services
DataServiceVersion: 1.0;NetFx
MaxDataServiceVersion: 2.0;NetFx
x-ms-version: 2011-08-18
x-ms-date: Mon, 28 Nov 2011 02:23:29 GMT
Authorization: SharedKeyLite devstoreaccount1:L7m4R/hSpwjR2vQ4Am95JWJcNi4vIGmNUAIcQs7mbX8=
Accept: application/atom+xml,application/xml
Accept-Charset: UTF-8
Host: 127.0.0.1:10002

この原因はもちろんIQueryable#AsEnumerableによってIQueryableをIEnumerableに変換したことによって、Where条件の追加がクエリ式への追加でなく、データ列に対するフィルタ条件の追加に変わったこと、そして、Linq to Azure Tableが最大でも1000件までしかデータを返さないという制限によるものです。

処理順に説明すると、「Linq to Azure Tableにおいて条件なしのクエリを投げる → 条件なしなので、PartitionKey,RowKey順に上位1000件が返される → 返ってきた1000件にはRowKeyが2000以上のデータは含まれていない → それに対してRowKeyが2000以上のもの上位3件を探しても見つからない → 取得データ件数が0になる」といった流れになります。

AsEnumerableを使わずにIQueryableをIEnumerabe型として返した場合

上記のようにIQueryable#AsEnumerableを使って明示的にIEnumerableに変換する場合は問題に気づきやすいですが、IQueryableを構築して返すメソッドを作った場合に戻り値の型がIEnumerableになっていると、AsEnumerableを使用しなくても、戻り値を利用している側ではIEnumerable(Linq to Object)扱いになることに注意が必要です。

例えば以下のようなメソッドを作成してPartitionKeyの条件を常に入れるような場合に、戻り値の型にIEnumerableをつかった場合です。このメソッドを使用して上記サンプルと同じ処理を行うと、PartitionKey=0まではLinq to Azure Tableとして処理されるものの、それ以降のRowKeyの条件およびTake(3)の処理がLinq to Objectに対するものとなります。

  public static IEnumerable<Data> Get(TableServiceContext context) {
   return from e in context.CreateQuery<Data>("Entities")
       where e.PartitionKey == "0"
       select e;
  }

まとめ

IQueryable、IEnumerableのどちらに対してLinq式の呼び出しを行なっているかによって、動作が異なってくることについて説明しました。今回説明したようなケースでは、データ件数が1000件以下のときは特に問題もなく動作しますが、1000件を超えた場合にのみ起きる潜在的なバグとなってしまいます。

また実際のシステムを構築するにあたっては、ビジネスロジック中のどのレイヤーまでをLinq to Azure Tableで扱うかなどLinq to Azure TableとLinq to Objectの使い分けに関するルールを設定したほうがいいかもしれません。

0 件のコメント:

コメントを投稿