Edited at

GAE/Goのdatastoreの挙動について

More than 3 years have passed since last update.

GAE/J+Slim3の語彙・知識を元にここに解説を書く。

GAE/Goの知識とGo言語の知識が混ぜこぜで書かれているかあまり気にしてはいけない。

以下の調査結果を得るためのテストコードはここに置いた。


EntityにKeyは付属してこない

structを定義する時に、そのstructに自分自身のKeyを持たせる方法はない。

EntityにIdまたはNameを自分で定義して、Putする時、Getした後にそこに忘れずにId, Nameを取り出したり移し替えたりして頑張る。

これを自動でやってくれるライブラリがgoonである。


IncompleteKeyはPutした後でも値は変わらない

key := datastore.NewIncompleteKey(c, "Test", nil)

newKey, err := datastore.Put(c, key, foo)
// keyはIncompleteのままだ!


変数宣言時、型がinterfaceならnilが入るがstructはポインタじゃないとnilが入らない

初期値をnilにしたい時 *Hoge にするべきか Hoge にするべきか一瞬で判断する方法は多分ない。

型名を見てinterfaceかstructか(に最終的に行き着く気がする)を見て判断する。

というかdatastore的には基本的にポインタ受け付けないぽい( *datastore.Key だけが例外かな?)


time.Timeなフィールドについて

*time.Timeはそもそもサポートされない。

必ずtime.Timeを使う必要がある。

nilかどうか?的な判断はIsZero()を使って初期化済みか判断するのが良さそう。


Entityのプロパティに[]*datastore.Keyを持ってBatchGet

普通にできる

foos = make([]Foo, len(bar.Keys))

err = datastore.GetMulti(c, bar.Keys, foos)


Entityのプロパティがスライスの時、Queryでそのうちの1つをひっかけてEntityを取得する

普通にできる。


sliceにできる要素

ここに記載の型はsliceにしてもOK。

structはsliceにすると怒られる。

datastore: flattening nested structs leads to a slice of slices: field "Foo" みたいな。

structの中のstructの挙動を考えるとまぁわからなくもない。

ProtocolBufferにmapできないしね。[]byte にしてくれたりはしないようだ。

map[string]string とかはダメ。mapはダメだ!


Put, Getにhookする方法

とりあえずPropertyLoadSaverを使うのが良さそう。

追加の処理を行うにはよいが、一部のプロパティの変換に噛むのはだいぶめんどいし変なコード書かないといけなさそう。


structの中のstruct

structの中にstructを置いてdatastoreにPutすると普通にPutできる。

type Foo struct {

Num int
Str string
}
type Bar struct {
Foo1 Foo
Foo2 Foo
}

上記の構造で&Bar{}をPutすると、概ね以下のような意味の構造として処理されるようだ。

type Bar struct {

Foo1Num int `datastore:"Foo1.Num"`
Foo1Str string `datastore:"Foo1.Str"`
Foo2Num int `datastore:"Foo2.Num"`
Foo2Str string `datastore:"Foo2.Str"`
}

ベネ。


structに別のstructをembedした場合

type Sub struct {

Rev int
}

type Main struct {
Sub
Name string
Age int
}

は概ね以下のような構造として扱われるようだ。

type Main struct {

Rev int
Name string
Age int
}


structの構成(schema)を変えた時

減らすとエラーになる。

type Before1 struct {

A int
B int
}

type After1 struct {
A int
C int
}

Before1をPutしてAfter1としてGetするとdatastore: cannot load field "B" into a "sample.After1": no such struct fieldとか言われてしまう。

type Before2 struct {

A int
B int
}

type After2 struct {
A int
C int
DeprecatedB int `datastore:"B,noindex"`
}

仕方ないのでこうする。

一度生やしてしまったプロパティは永久に消せないんじゃないかこれ。

追記:さんとりーパイセンがstruct2つ用意して古いのでGetして新しいのでPutしたら消せるよ。って言ってた。たしかにそのとおりなのであるKindのEntityを一気にmigrateする時は消せそう。


Entityの内部構造確認したい

適当にオラッ!ってやって適当にdumpして調べればよい。

import (

"appengine"
"appengine/datastore"
pb "appengine_internal/datastore"
"code.google.com/p/goprotobuf/proto"
)

func GetProtoMulti(c appengine.Context, keys []*datastore.Key) (*pb.GetResponse, error) {
pbKeys := make([]*pb.Reference, len(keys))
for idx, key := range keys {
n := 0
for i := key; i != nil; i = i.Parent() {
n++
}
e := make([]*pb.Path_Element, n)
for i := key; i != nil; i = i.Parent() {
n--
kind := i.Kind()
e[n] = &pb.Path_Element{
Type: &kind,
}
if key.StringID() != "" {
name := key.StringID()
e[n].Name = &name
}else if key.IntID()!=0 {
id := key.IntID()
e[n].Id = &id
}
}

namespace := key.Namespace()
pbKeys[idx] = &pb.Reference{
App: proto.String(c.FullyQualifiedAppID()),
NameSpace: &namespace,
Path: &pb.Path{
Element: e,
},
}
}

req := &pb.GetRequest{
Key: pbKeys,
}
res := &pb.GetResponse{}
if err := c.Call("datastore_v3", "Get", req, res, nil); err != nil {
return nil, err
}

return res, nil
}


Slim3のrevision的なやつ

無いので気合で頑張る。

Revフィールド作って、PropertyLoadSaverとかで保存前とか読み出し時に頑張ってしこしこマイグレーションすると良さそう。

まだその方針で実装したことはないけど。


GetMultiで存在しないEntityのKeyを指定した時

appengine.MultiErrorにキャストしてぐりぐりチェックするのだ!と教えてもらった。(あざます!)

なお、goonのGetMultiでも同様の結果になるよう配慮されている模様。


おわり

コレ以外にこういう特長あるよー!とかの情報があったらコメントとかで教えてください。

おなしゃす!