GoonはGoogle Cloud Datastoreを読み書きするための薄いラッパーパッケージで、GAE/Go+Datastoreの環境で力を発揮します。詳細は以下の記事が参考になります。
そんなGoonですが、表題の通りプロジェクションクエリとの相性が良くありません。クエリの種類に応じてfunc (q *Query) Run(c context.Context)
とfunc (g *Goon) Run(q *datastore.Query)
とをプログラマが使い分ける必要があります。
プロジェクションクエリとは
Datastoreはカラム指向のNoSQLで、インデックス機能によるデータのソートやフィルタにも対応しています。また、RDBMSで言うマルチカラムインデックスを作ることもできます(カスタムインデックス)。
さらに、検索クエリがRDBMSで言うCovering indexになっている(=インデックスだけで結果を返せる)場合は費用面で低コストになるメリットがあります1。このようなクエリはプロジェクションクエリと呼ばれ、次のようなクエリで実現できます。
q := datastore.NewQuery("post").Project("bbs_id", "updated_at").Order("-updated_at")
この例では、post
というKindについて、bbs_id
とupdated_at
の2プロパティのみを取り出し、updated_at
で降順ソートして全件取り出しています。
プロジェクションクエリとGoonの相性
ところで、このクエリをGoonのRunメソッドで実行してしまうのは混乱の元です。
というのも、GoonはDataStoreに対するキャッシュ層を2段持っています。
- インプロセスのキャッシュ(1段目キャッシュ)
- memcached(2段目キャッシュ)
- Datastore
GoonのRun/Next/GetAllメソッドでデータを取得した場合、その結果は全てインプロセスのキャッシュに乗ってしまいます。ところが、プロジェクションクエリでは一部のプロパティについてのみ値がセットされ、取れなかったプロパティについては全てゼロ値が返ってきます。この不完全な結果がキャッシュされてしまうと、以後の処理で同じエントリの値を取得しようとしてもキャッシュヒットしてしまって正しい値が取れないトラブルが起こってしまうのです。
つまり、プロジェクションクエリの場合はdatastoreパッケージのRunメソッドを使ってキャッシュに乗らないようにし、通常のクエリの場合はGoonのRunメソッドを使ってキャッシュの恩恵を受ける、という使い分けが必要になります。
補足:Goonでキーオンリークエリを使う
ちなみに、キーオンリークエリについてはGoon側で考慮されているので、Goon経由で使っても問題はありません。
q := datastore.NewQuery("post").KeysOnly().Filter("bbs_id =", b.ID).Order("-updated_at")
t := g.Run(q)
for {
k, err := t.Next(nil)
// ....
}
このように、キーオンリークエリではNextメソッドの第一引数にnilを渡せばキャッシュされません。
また、GetAllメソッドでは内部的にキーオンリークエリかどうかの判定を行っており、結果がキャッシュされないように実装されています。
参考資料
-
「データストアのクエリ | Cloud Datastore のドキュメント | Google Cloud Platform」によれば「distinct on 句を使用しないプロジェクションクエリは小規模な操作であり、クエリ自体については 1 回のエンティティ読み取りとしてカウントされます」とのことなので、1000件取得しても1件読み込みのお値段ということになります。つまり、通常クエリをプロジェクションクエリにすれば最大99.9%プライスダウンです。オトクすぎますね。 ↩