読者です 読者をやめる 読者になる 読者になる

DatastoreのParentKey(AncestorQuery)の特徴と使い所(datastore when to use parentkey)

GCP

とはいえ、ちょっと自信ないので、
間違っている箇所があれば指摘してもらえると嬉しい。


ParentKeyはEntity同士を関連付けるキーのことで、
RDBでいう外部キーのようなイメージを持つかもしれないが、全然違う。
何も考えずにParentKeyでリレーションを貼ると面倒なことになる。

とりあえず以下の2点は抑えておく必要がある。
https://cloud.google.com/appengine/docs/go/datastore/entities#Go_Ancestor_paths
https://cloud.google.com/appengine/docs/go/datastore/entities#Go_Transactions_and_entity_groups

日本語訳は以下にまとまっている。
「Ancestor paths」と「Transactions and entity groups」を
中心にチェックするといい。
http://blog.sojiro.me/blog/2016/08/07/about-gae-datastore/

以下はDatastoreのデータ設計について。
自分はこれでDatastoreのデータ構造を理解できた気がするので、
以下も確認しておくといい。
http://googlecloudplatform-japan.blogspot.jp/2015/08/google-cloud-datastore.html


「Userが複数のTaskを持つ」というデータ構造を例に特徴をまとめてみる。

User Kind

key, name, email
---------------------
1, xxx, xxx@email.com
2, xxx, yyy@email.com

Task Kind

key, ParentKey, title
----------------------
1, Key(User, 1), my_task_1
2, Key(User, 1), my_task_2
1, Key(User, 2), my_task_1
----------------------

このデータ構造では
Task に ParentKey を利用しているので、
User と Task が EntityGroup になっている。

1.常に最新のEntity群を取得できる

StrongConsistency になるので、
常に最新の Entity を取得することができる。
Get(), GetMulti() も StrongConsistency だが、
Ancestor().Filter() のようにキー以外の検索条件で最新の Entity を取得できる。

2.トランザクション内でも特定の User に紐づく Task の Entity を取得できる

トランザクション内でEntityを取得する際に利用できるのは
キーを用いた Get(), GetMulti() と Ancestor() を利用した AncestorQuery だけ。
Ancestor() を指定しないクエリ(Query().Filter() を利用したクエリなど)はエラーになってしまう。
ちなみに、Datastore の COUNT() はトランザクション内で利用できた。

エラーについてはこちらでも説明しています。
http://d.hatena.ne.jp/pospome/20161108/1478611356

トランザクション内では Key ベースの Get(), GetMulti() を利用することが多いので、
あまり気にしなくてもいいかもしれない。
仮に Filter() による検索が必要になっても、
Ancestory() を利用して親Entityに紐づく Entity を取得することしかできないので、
Kind 全体を検索対象にすることはできない。


3.トランザクションで EntityGroup 単位で楽観ロックが効く

Datastore のトランザクションは RootEntity が「EntityGroup が更新された TimeStamp」を持ち、
それをベースにした楽観ロックになっている。
なので、User の更新によって TimeStamp が更新されたタイミングと、
Task の更新が重なった場合、
Task の更新処理は失敗してしまい、トランザクションのリトライが走る。

このように EntityGroup で楽観ロックが効くので、
EntityGroup ではデータの整合性を保つことができる。

ただし、EntityGroup の更新処理が頻繁に発生すると、
トランザクションがリトライする可能性が高くなり、
更新処理が失敗することが多くなるので注意。

複数の EntityGroup にまたがったトランザクション自体は可能なので(最大25個まで)、
Task に ParentKey を持たせなくても
トランザクションをかけることは可能だが、
楽観ロックの TimeStamp が User, Task で別々になるので、
User + Task 単位で整合性を保つようにするには
ParentKey を利用する必要がある。

今回の User + Task の例では、この特性は必要ないと思うし、
どのようなケースでこの特性が必要になるのかパッとイメージできない。


4.キーで引く場合に ParentKey が必要になる

ParentKey を持つ Entity は ParentKey + Key でユニークになる。
最初に載せた Task Kind の Entity を見ると、
key = 1 の Entity が2つあるが、ParentKey が異なるので、間違っているわけではない。

ParentKey を持つと、
特定の Task を取得するには User の key と task の key をセットで持っていなければいけない。
RDBだと id 1つで引けるようにすることが多いので、
ここは少し不便に感じるかもしれない。



5.EntityGroup に対して登録、更新、削除できるのは1秒に1回のみ

これは StrongConsistencyを実現するため。
Task の登録、更新、削除は1秒間に1回しかできない。

今回のように User ベースで Task を持つ場合は
1人のユーザーが1秒間に複数のタスクを登録するとは考えづらいので、
この制約は問題ないと思う。

ただし、User が Company に変わって、
1つの企業のタスクを管理するシステムになってしまうと、
複数の従業員によって1秒間に複数のタスクを登録するケースが考えられるので、
この制約が問題になってくる。

3番の問題もあるので、
EntityGroup はユーザー単位のように小さな粒度に保つのが基本。



特徴の説明は以上。

1,2,3 がメリットで、4,5 がデメリットになる。
1,2,3 のどれか1つが必要であれば、
ParentKey を利用して AncestorQuery を利用できるようにする必要があるが、
不要であれば、ParentKey を利用する必要はない。