kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

タグアーカイブ: 携帯

jp110311 の開発で得られた ASP.NET / 携帯 / Azureでの開発ノウハウあれこれ


サンプルをちょっと書いて乗せてみる程度では得られなかった知見という事で簡単に箇条書きしてみます。

Azure Table の continuation は少なくとも Web Frontend なアプリケーションでは無意味だし不要

今回は携帯対応という事で、どうせ表示できるのは多くても数十件です。この時に1000件単位での continuation はおおよそ無意味で、バッチで舐めるような操作でも作らない限りには不要です。

んで、ページング操作ですが、基本的には RowKey を並べたカラムを持つレコードをインデックスレコードとして作り、このインデックスに並んだ RowKey を or で並べて select します。((PartitionKey=’PK1’) and (RowKey=’RK1’)) or ((PartitionKey=’PK2’) and (RowKey=’RK2’)) を選択するのは普通には以下の形になります。

はい、いきなり式木操作です。

var qidx = context.CreateQuery< Indexのエンティティ型 >( “table” );
var indexEntity = qidx.Where( idx=>idx.PartitionKey== パーティションキー
                   && idx.RowKey== “INDEX” ).SingleOrDefault();
if( indexEntity==null ) return Enumerable.Empty< 戻りエンティティ型 >();

ParameterExpression param = Expression.Parameter( typeof( 戻りエンティティ型 ) );
MemberInfo miPartitionKey = typeof(戻りエンティティ型).GetProperty( “PartitionKey”);
MemberInfo miRowKey = typeof(戻りエンティティ型).GetProperty( “RowKey”);
Expression predBody = null;

foreach( var k = indexEntity.IndexColumn.Split( “;” ) )
{
    if( predBody==null )
    {
        predBody = Expression.And(
             Expression.Equal( Expression.MakeMemberAccess( param,miPartitionKey ),
                                           Expression.Constant( パーティションキー, typeof(string) )),
             Expression.Equal( Expression.MakeMemberAccess( param,miRowKey ),
                                           Expression.Constant( k, typeof(string ) );
    }
    else
    {
        predBody = Expression.Or(
             predBody,  
             Expression.And(
               Expression.Equal( 
                        Expression.MakeMemberAccess( param,miPartitionKey ),
                        Expression.Constant( パーティションキー, typeof(string) )),
               Expression.Equal( Expression.MakeMemberAccess( param,miRowKey ),
                                             Expression.Constant( k, typeof(string ) );
    }
}
if( predBody==null ) return Enumerable.Empty<戻りエンティティ型>();
var pred = (Expression<Func< 戻りエンティティ型,bool >>)
                 Expression.Lambda( predBody, param );
var q = context.CreateQuery< 戻りエンティティ型 >(“table”);
return q.Where( pred ).AsEnumerable();

この (PartitionKey and RowKey) or (PartitionKey and RowKey ) といように パーティションキー、 ローキーをそれぞれ評価しての形のクエリだと、10件程度をとれるクエリまでは正しく処理されるようです。それ以上になるとクエリが長すぎる的エラーで結果が返りません。(クエリの長さなので、パーティションキーやローキーのデータ項目長によって可能な件数が変わるでしょう)

これを変形して PartitionKey=X and ( RowKey=A or RowKey=B ) の様に RowKey だけを or 続きでつないでいくことができます。この形式でとる方が PartitionKey が限定されてエンティティグループトランザクションがかかりやすい、分散クエリにならないはずなので早いという事になります。都度都度 PartitionKey を指定しない分 or 項数を増やせ結果的に一発でとれる件数が増えます。

RowKeyの設計次第ですが、これでとれるのは最大でも数十件(多くて30ぐらい)という所になります。

普通の Web アプリの場合にはこれで数十件が取れれば十分よね(アプリケーションの画面でのページング単位は大抵の場合数十件)って事で、アプリケーション画面の設計に合致しやすい形になります。

50件ぐらいをとりたい場合はフェッチ密度によって戦略が解れます。フェッチ密度が十分に高い場合(各行のサイズが小さく、選択範囲内で75%以上の行をフェッチするなら)にはRowKey の Min/Max で範囲を抽出し、indexで示すレコードのみにフェッチ後にフィルタします。

var qidx = context.CreateQuery< Indexのエンティティ型 >( “table” );
var indexEntity = qidx.Where( idx=>idx.PartitionKey== パーティションキー
&& idx.RowKey== “INDEX” ).SingleOrDefault();
if( indexEntity==null ) return Enumerable.Empty< 戻りエンティティ型 >();
var rowKeys = indexEntity.KeyListColumn.Split(“;”).Skip(start).Take(max).ToArray();

string minKey = rowKeys.Min();   // Enumerable.Min/Max には string 版はありません
string maxKey = rowKeys.Max(); // 拡張メソッドで増やしてあげて下さい。
var q = context.CreateQuery< 戻りエンティティ型 >(“table”);
return q.Where( item=> item.PartitionKey==パーティションキー
                && string.Compare( item.RowKey, minKey)>=0
                && string.Compare( item.RowKey, maxKey)<=0 )
            .AsEnumerable()
            .Where( item=> rowKeys.Contains( item.RowKey ) );

フェッチ密度が低い場合は最初のアプローチでの RowKey 指定で複数列を選択するのをループする形になります。

INDEX ってレコードもってないよ!って場合には実際としてどんな RowKey でとればいいかは自然キーで推測可能という事でない限り解りません、1000件帰ってくるかもしれないクエリ打ってみるしかないになりますので、面倒でも何等かINDEXレコードを持たないと多分性能は出ません。

 

サービスクラスの Testability(テスト可能性)を確保するためのパターン

BbsEntityServicesに実装されています。

http://jp110311.codeplex.com/SourceControl/changeset/view/7674#90653

  1. 開発ストレージに振り向けるための仕組みを持ちましょう。
  2. SaveChanges の発行はメソッドを用意し集約し、SaveChanges発行前イベントを持ちましょう。

1 は開発ストレージにテストでのアクセスを振り向ける為に必要です。CloudStorageAccount.FromConfigurationSetting で通常 CloudStorageAccount を取得しますが、このメソッドは RoleEnvironment がしっかりあって、アプリケーションの初期化コードをちゃんと通らないと動作しません。(SEH例外というイヤーな物が飛んできます)

要するに普通に MsTest テストコンテナでは動くはずもない物なので単純にそれを使ってストレージアカウントにアクセスしているとテストコンテナ内では動きません。

Azure SDK 1.3 まででは開発ストレージではエンティティグループトランザクションは未サポートで、動作しないだけでなく、エンティティグループトランザクションの指定である SaveChangesOption.Batch を指定するとエラーになります。このため、開発ストレージではエンティティグループトランザクションの実際の動作テストはおろかエンティティグループトランザクションを利用するコードのテストもできません。(all or nothing にならないだけでいいのに)

このため、開発ストレージを使っている場合にはエンティティグループトランザクションをかけないようにするコード対応が必要となります。

2 はエンティティグループトランザクションの衝突等のテストをする為に必要です、発行前イベントでわざと競合するような更新をしたうえでエンティティグループトランザクションが適切に all or nothing に落ちていることを確認する必要があります。

 

UseUri でのセッションステートを使う時は POST を受ける前にセッションを保存させなければならない

jp110311 では UseUri で携帯向けにクッキーを利用しないセッションステートを設定しています。この場合にセッションに何も保存していない場合、 POST 要求を初めて受けるとASP.NETはSessionID を確定させようとし、セッションが未保存であれば保存した上でリクエストをリダイレクトします。

このリダイレクトによって POST が失われてしまい、色々な操作が失敗する事になりました。

http://jp110311.codeplex.com/SourceControl/changeset/view/7674#94558

BaseController.cs の OnActionExecuting が回避方法です。要するに新規セッションであれば無意味でもとりあえず値を書き込んで、Dirty にし Session がリクエストの終了時に保存されるようにしています。

実は jp110311 のサイト全体として、セッションはログイン維持の為にしか使っていません、ASP.NETの認証インフラがセッションを使っているだけで、アプリケーションとしては使っていないという形です。「セッションなんて使わねーよ、ばーかばーか」です。

Azure Storage を使うセッションステートプロバイダは独自実装しようと思ったんですが、first release をとにかく早く出したかったって事で出来あいの物を使ってます。

こいつはいくつか問題を抱えてます。

  1. 770行目あたり、 itemData と staticsData を reader.ReadLine で読むところ、セッション保存されてない場合にはこれらは null になりますが Convert.FromBase64String に渡してて ArgumentNullException を飛ばされます。
  2. Expire処理が全般として実装されてませんので、溜まるいっぽうになります。
  3. StorageAccount 回りのテスト可能性の所が全く考慮されてないので、テストが死ねます

 

UseUri でのセッションステートを使う場合 HtmlHelper.BeginForm() は使えない

使えないのはパラメーター無しのメソッド、HtmlHelper.BeginForm( null ) で!でないとフォームのactionがUrlセッションがついてないアドレスに飛ばされ、リダイレクトされてPOSTデータが取れない。