[GAE/Datastore] エンティティの関連付けパターン3つ

# はじめに
Datastoreを使っていて、**エンティティ間の関連をどう表現するか**は非常に悩みました。

その記録を、具体例+長短でまとめてみました。
なお、部分的に[goon](https://godoc.org/github.com/mjibson/goon)を使用しています

# 参考
【公式ドキュメント】
・[データの整合性](https://cloud.google.com/datastore/docs/concepts/structuring_for_strong_consistency)
・[強整合性と結果整合性のバランス](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore/) [^1]
・[Ancestorパス](https://cloud.google.com/datastore/docs/concepts/entities?hl=ja#ancestor_paths)
・[トランザクションとエンティティグループ](https://cloud.google.com/datastore/docs/concepts/transactions?hl=ja#transactions_and_entity_groups)
・[Datastoreでのデータ整理の考え方](https://cloudplatform-jp.googleblog.com/2015/08/google-cloud-datastore.html)

【その他】
・[DatastoreのParentKeyの特徴と使い所](http://pospome.hatenablog.com/entry/20161009/1475990332)
・[Datastore Design Pattern](https://docs.google.com/presentation/d/1ogO88RFNY3guT8p_UsPHzA8lTNGwyjjNDz0-0iFwZlQ/edit#slide=id.g2b2c78386d_0_20)

# 想定場面 (Tweetを例に)
例えば、以下を実現したいとする。

- UserがTweetをする
- UserはTweetにいいね(Like)できる

この時、「*Tweetエンティティをどう定義しようか*」という話。
Likeと違って、Tweetエンティティは「**ある1つに特定しなきゃいけない場面が多い**」です。(LikeやCommentの対象になるため)

# 1. UserをParentとする
```dataobj.go
type User struct {
    ID string `datastore:"-" goon:"id"`
}

type Tweet struct {
    ID       int64          `datastore:"-" goon:"id"`
    User     *datastore.Key `datastore:"User" goon:"parent"`
    Contents string         `datastore:"Contents"`
}

type Like struct {
    User  *datastore.Key `datastore:"User" goon:"parent"`
    Tweet *datastore.Key `datastore:"Tweet"`
}
```

☆ ParentKeyは、RDBの外部キーとは**完全に別物**であり、「<font color="RED"><strong>強整合性</strong></font>」が必要とされる場面で使う。

## 長短
<dl>
  <dt>◯ Userは自身に関することであれば、常に最新の情報を得られる。</dt>
    <dd>自身のTweetやいいね一覧は最新であることが保証される</dd>
  <br/>
  <dt>◯ トランザクション内でAncestorQueryできる</dt>
    <dd>とはいえ、トランザクションオプションを使えば、複数のEntity Groupにまたがってのトランザクション実行は可能</dd>
  <br/>
  <dt>△ Entity Groupの更新は1回/秒の制限がある。</dt>
    <dd>読取が強い整合性を持つ一方、書込には制限ができる。今回であれば、Userは1秒間に1回しかTweetできない。</dd>
  <br/>
  <dt>✖️ある1つのTweetにLikeするのが面倒。</dt>
    <dd>Tweetを特定(=Keyを取得)するには、<font color="ORANGE">「TweetのParentKey」&「TweetのID」</font>が必要</dd>
</dl>

## サンプルコード
```query.go
// 特定のTweetのKeyを取得する
tk := datastore.NewKey(ctx, "Tweet", "", [対象TweetのintID], [対象TweetのParentKey])

// いいねする
like := &dataobj.Like{User: [いいねしたユーザーのKey], Tweet: tk}
g.Put(like)
```

```go
// **** 余談1 ****
// 複数のEntity Groupにまたがる場合は以下のオプションを使う
option := &datastore.TransactionOptions{XG: true}
// こんな感じでトランザクションを実行する
err := g.RunInTransaction(func(g *goon.Goon) error { Putとかする }, option)

// **** 余談2 ****
// 以下はIDの数値が得られるだけでParent情報を含まないため、完全な識別子とはならない
tk.IntID()
```

# 2. プロパティでKeyを持たせる
```dataobj.go
type User struct {
    ID string `datastore:"-" goon:"id"`
}

type Tweet struct {
    ID       int64          `datastore:"-" goon:"id"`
    User     *datastore.Key `datastore:"User"`
    Contents string         `datastore:"Contents"`
}

type Like struct {
    User  *datastore.Key `datastore:"User" goon:"parent"`
    Tweet *datastore.Key `datastore:"Tweet"`
}
```

☆ 強整合による一貫性を必要としない(**トランザクション用の囲いが不要かつ結果整合性が許容できる**)場合は、**2か3**を考えるのが基本だと理解しています。[^2]

## 長短
<dl>
  <dt>◯ 特定のTweetを取得しやすい</dt>
    <dd><font color="ORANGE">IDだけ</font>でKeyを取得できる。</dd>
  <br/>
  <dt>△ TweetとLikeにまたがったトランザクションを実行するには、オプション指定が必要</dt>
    <dd>先述の通り</dd>
  <dt>✖️Userは自身に関することでも、常に最新情報を得られるとは限らない</dt>
    <dd>Entity Groupを組んでいないため</dd>
</dl>

## サンプルコード
```query.go
// 特定のTweetのKeyを取得する
tk := datastore.NewKey(ctx, "Tweet", "", [対象TweetのintID], nil)

// いいねする
like := &dataobj.Like{User: [いいねしたユーザーのKey], Tweet: tk}
g.Put(like)
```

# 3. Tweetエンティティにsliceとして持たせる
Tweet主であるUserのKeyだけでなく、**Like情報もTweetエンティティに保持させてしまう**

```dataobj.go
type User struct {
    ID string `datastore:"-" goon:"id"`
}

type Tweet struct {
    ID        int64            `datastore:"-" goon:"id"`
    PostUser  *datastore.Key   `datastore:"PostUser"`
    Contents  string           `datastore:"Contents"`
    LikeUsers []*datastore.Key `datastore:"LikeUsers"`
}
```
## 長短
<dl>
  <dt>◯ エンティティの書込・読取が減る</dt>
  <br/>
  <dt>✖️沢山いいねされるとエンティティが膨れ上がる</dt>
  <br/>
  <dt>✖️「いいね」の情報<font color="ORANGE">だけ</font>をGetすることはできなくなる(*1)</dt>
</dl>

*1: [Projection Query]((https://cloud.google.com/appengine/docs/standard/go/datastore/projectionqueries))で行うことは可能

## サンプルコード
```query.go
// 特定のTweetのKeyを取得する
tk := datastore.NewKey(ctx, "Tweet", "", [対象TweetのintID], nil)

// Tweetエンティティを取得
tw := &dataobj.Tweet{ID: tk.IntID()}
g.Get(tw)

// いいねする
tw.LikeUsers = append(tw.LikeUsers, [いいねしたユーザーのKey])
g.Put(tw)
```

# おわりに
アドバイスがんがん下さい！:bow_tone1:

[^1]: `「エンティティ グループは、1つのルートエンティティと、その子や継承者から構成されます　」`と記述があるように、親子関係を持たないルートエンティティもそれ単体でEntity Groupとみなされる。筆者は最初勘違いしていた...
[^2]: ちなみに、場合によってはKeyにあらゆる情報を埋め込み、Keyのみの取得で事を済ませてコストを抑える手法も採られるようです。