2012年6月5日火曜日

GAE/Gで時間のチェック(Datastore編)

GAE/GのSDK 1.6.4からGo Ver 1が適用され、
時間をデータストアに保存する時は、datastore.Time(int64のtype。今は削除されている)では無く、time.Timeが利用できるようになっています。(参考Blog記事

time.Timeをデータストアに格納しようと思うと、普通に日時が設定されていれば問題ないのですが、
例えば、データストアのプロパティの追加した時など、日時のプロパティに時間が設定されていない時があります。

日時が設定されていないまま、Putを行うと、"time value out of range"のエラーが
発生してしまいます。

これを回避するためには、

  1. PropertyLoadSaverのLoadかSaveで日付をセットする。
  2. リリース前にデータの移行をしておく。(未設定の日時が発生しないようにしておく)
のどちらかになると思います。2のケースだと全件Putされるので問題ないと思いますが、
移行プログラムでデータを移行させない事の方が多いと思います。

したがって、PropertyLoadSaverインターフェイスを実装して、以下のように、
時間がZeroかどうかを判定するロジックを入れておかないと、Putが成功しません。
判定メソッドはGo言語標準で用意されているIsZero()を利用します。
LoadとSaveどちらで設定するのも構わないと思いますが、ここでは、Load時に設定する
方針にしてみます。(if文1つなら処理コストはあまり変わらないだろうと言っても、読み込み時に毎回実行するのは無駄だ。と思われる場合はSaveで実装するのが良いでしょう)

func(hoge *Hoge) Load(c <-chan datastore.Property) error {
        // データストアから読み出し。
 if err := datastore.LoadStruct(hoge, c); err != nil {
  return err
 }

 if hoge.Date.IsZero() {
  // データがない(0000/01/01状態)ものは現在時刻にするなど。
  hoge.Date = time.Now().Format(TimeFormat)
 }

 return nil
}

データが既に入っている場合はIsZero()がfalseになると思われるので、
特に時間の設定は不要(データストアから読みだした時点で代入されている)でしょう。


GAE/GでバッチGetをした時の注意点

最近、Androidアプリ「ええことなかったわー(β)」を開発したのですが、
実は、データを貯めているサーバ側はGAE/Gで開発しています。

本当に、レスポンスとしては早いですよ。
今のところ、リクエストに対するレスポンスは、
ms単位の時間でレスポンスを返しています。
(GAE/Jでも仕事でサービスを利用していますが、レスポンスの内容が全然違いますが、1s前後のオーダだったりするので…)

先ほど、Admin ConsoleでのPerformanceを最弱にしていなかったので、
まだユーザ数も少ないので、最弱(Max Idle Instance = 1+Min Pending Latency = automatic)にしてみました。その結果は後日…ということで。

さて、本題のバッチGetを利用するときの注意点なのですが、
結論から言うと、「指定したキーのエンティティが1件でも無いと、appengine.MultiErrorを返す」ということです。
実際には、以下のようなコードになると思いますが

data := make([]HogeEntity, len(keys))
if err := datastore.GetMulti(c, keys, data); err != nil {
// 何かのエラー
}

全てのキーのエンティティがあれば、エラーの判定にならない(戻り値=nil)なのですが、
1つでもエンティティが存在しない場合、全てエラー(戻り値 = appengine.MultiError)となります。
(おかげで、全部エラーレスポンスに…)

で、これは困ったという事で、以下のようなチェックを行うことになります。

data := make([]HogeEntity, len(keys))
if err := datastore.GetMulti(c, keys, data); err != nil {
 if _, ok := err.(appengine.MultiError); ok {
  // バッチGetだと指定したキーのエンティティが1個でもない場合、MultiErrorが返る。
  c.Infof("Hoge NoSuchEntity")
  return keys, data, nil
 } else {
  c.Errorf("Batch Get Error %s", err.Error())
  return keys, nil, err   
 }
}

実際、appengine.MultiErrorが返されても、存在しているデータは取得できるので
取得したデータが構造体にセットされている部分と、空の部分がsliceに存在する
状態になるので、後で、空の部分を除外するなど、編集処理が必要となります。

ちなみに、appengine.MultiErrorはtype MultiError []errorと定義されていますので
複数のエラーが含まれた状態になることも考慮しておく方が良いかもしれません。
私はGetだけの利用だったので、今のところ、正常なものは利用しています。

MultiErrorの戻り値はGetMulti/PutMulti/DeleteMultiで戻される事があるという
ことなので、(ドキュメントのBasic Operationsに明記)利用するときはappengine.MultiErrorの中身を確認してみてください。