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


GAE/J+Slim3の語彙・知識を元にここに解説を書く。
GAE/Goの知識とGo言語の知識が混ぜこぜで書かれているかあまり気にしてはいけない。

以下の調査結果を得るためのテストコードは[ここ](https://github.com/vvakame/appengine-go-datastore-sample/blob/master/src/sample/datastore_test.go)に置いた。

## EntityにKeyは付属してこない

structを定義する時に、そのstructに自分自身のKeyを持たせる方法はない。
EntityにIdまたはNameを自分で定義して、Putする時、Getした後にそこに忘れずにId, Nameを取り出したり移し替えたりして頑張る。

これを自動でやってくれるライブラリが[goon](https://github.com/mjibson/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にできる要素

[ここ](https://cloud.google.com/appengine/docs/go/datastore/entities#Go_Properties_and_value_types)に記載の型は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](https://cloud.google.com/appengine/docs/go/datastore/reference#hdr-The_PropertyLoadSaver_Interface)を使うのが良さそう。
追加の処理を行うにはよいが、一部のプロパティの変換に噛むのはだいぶめんどいし変なコード書かないといけなさそう。

## 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にキャストしてぐりぐりチェック](https://gist.github.com/hogedigo/c62ea6ac71d7ad84c1b8)するのだ！と教えてもらった。(あざます！)

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

## おわり

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