Go
GoogleAppEngine
GoogleCloudPlatform

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


はじめに

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とする


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の外部キーとは完全に別物であり、「強整合性」が必要とされる場面で使う。


長短


◯ Userは自身に関することであれば、常に最新の情報を得られる。

自身のTweetやいいね一覧は最新であることが保証される



◯ トランザクション内でAncestorQueryできる

とはいえ、トランザクションオプションを使えば、複数のEntity Groupにまたがってのトランザクション実行は可能



△ Entity Groupの更新は1回/秒の制限がある。

読取が強い整合性を持つ一方、書込には制限ができる。今回であれば、Userは1秒間に1回しかTweetできない。



✖️ある1つのTweetにLikeするのが面倒。

Tweetを特定(=Keyを取得)するには、「TweetのParentKey」&「TweetのID」が必要


サンプルコード


query.go

// 特定の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を持たせる


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


長短


◯ 特定のTweetを取得しやすい


IDだけでKeyを取得できる。



△ TweetとLikeにまたがったトランザクションを実行するには、オプション指定が必要

先述の通り

✖️Userは自身に関することでも、常に最新情報を得られるとは限らない

Entity Groupを組んでいないため


サンプルコード


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"`
}



長短


◯ エンティティの書込・読取が減る



✖️沢山いいねされるとエンティティが膨れ上がる



✖️「いいね」の情報だけをGetすることはできなくなる(*1)

*1: Projection Queryで行うことは可能


サンプルコード


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のみの取得で事を済ませてコストを抑える手法も採られるようです。