はじめに
Datastoreを使っていて、エンティティ間の関連をどう表現するかは非常に悩みました。
その記録を、具体例+長短でまとめてみました。
なお、部分的にgoonを使用しています
参考
【公式ドキュメント】
・データの整合性
・強整合性と結果整合性のバランス 1
・Ancestorパス
・トランザクションとエンティティグループ
・Datastoreでのデータ整理の考え方
【その他】
・DatastoreのParentKeyの特徴と使い所
・Datastore Design Pattern
想定場面 (Tweetを例に)
例えば、以下を実現したいとする。
- UserがTweetをする
- UserはTweetにいいね(Like)できる
この時、「Tweetエンティティをどう定義しようか」という話。
Likeと違って、Tweetエンティティは「ある1つに特定しなきゃいけない場面が多い」です。(LikeやCommentの対象になるため)
1. UserをParentとする
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の外部キーとは完全に別物であり、「強整合性」が必要とされる場面で使う。
長短
- ◯ Userは自身に関することであれば、常に最新の情報を得られる。
- 自身のTweetやいいね一覧は最新であることが保証される
- ◯ トランザクション内でAncestorQueryできる
- とはいえ、トランザクションオプションを使えば、複数のEntity Groupにまたがってのトランザクション実行は可能
- △ Entity Groupの更新は1回/秒の制限がある。
- 読取が強い整合性を持つ一方、書込には制限ができる。今回であれば、Userは1秒間に1回しかTweetできない。
- ✖️ある1つのTweetにLikeするのが面倒。
- Tweetを特定(=Keyを取得)するには、「TweetのParentKey」&「TweetのID」が必要
サンプルコード
// 特定のTweetのKeyを取得する
tk := datastore.NewKey(ctx, "Tweet", "", [対象TweetのintID], [対象TweetのParentKey])
// いいねする
like := &dataobj.Like{User: [いいねしたユーザーのKey], Tweet: tk}
g.Put(like)
// **** 余談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を持たせる
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
長短
- ◯ 特定のTweetを取得しやすい
- IDだけでKeyを取得できる。
- △ TweetとLikeにまたがったトランザクションを実行するには、オプション指定が必要
- 先述の通り
- ✖️Userは自身に関することでも、常に最新情報を得られるとは限らない
- Entity Groupを組んでいないため
サンプルコード
// 特定の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エンティティに保持させてしまう
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"`
}
長短
- ◯ エンティティの書込・読取が減る
- ✖️沢山いいねされるとエンティティが膨れ上がる
- ✖️「いいね」の情報だけをGetすることはできなくなる(*1)
*1: Projection Queryで行うことは可能
サンプルコード
// 特定の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)
おわりに
アドバイスがんがん下さい!