Cloud Datastoreをざっくり理解する
スケーラビリティの高い NoSQL データベース
いま友人と開発しているプロダクトで、本格的にDatastoreを使った開発をすることになったので、概念を理解するためのメモ記事です。
RDBから理解するDatastore概念
概念 | Datastore | RDB |
---|---|---|
オブジェクトのカテゴリ | Kind | Table |
1つのオブジェクト | Entity | Row |
1つのブジェクとの各データ | Property | Field |
オブジェクトに対するユニークなID | Key | Primary Key |
これらを分かりやすい説明をしてくれていたものがあったので、引用させて頂きます。
Kindと呼ばれるテーブルみたいなものに
Entityと呼ばれるオブジェクトを格納できます。
Entityには一意のKeyが割り当てられています。
EntityのフィールドはPropertyといいます。
つまり、Kind -> Entity -> Propertyという親子関係のようです。
GAE/GO Cloud Datastoreを使ってみる | teppeeeのブログ
次に実行されるメソッドの種類について
動作 | Datastore | RDB |
---|---|---|
取得 | Get, GetAll | SELECT |
追加 | Put | INSERT |
更新 | Put | UPDATE |
削除 | Delete | DELETE |
基本的なCRUDは当然出来ます。ただ、Readに関しては読み取るだけなら問題はないですが、複雑な検索はRDBに比べると非力なので、Search APIを使って検索したりすることが一般的なようです。
Datastoreのデータ整理
cloudplatform-jp.googleblog.com
上記の記事が非常に分かりやすかったので、引用しながら理解していこうと思います。
Google Cloud Datastore のような非リレーショナル データベースにおけるデータのモデリングや保存には、固有の難しさがあります。結合や正規化など、リレーショナル データベースの有益な機能の多くは、非リレーショナル データについては、スケーラビリティに欠けるという理由で適用されません。リレーショナル データベースを「オブジェクトと関係」としてとらえるという一般的なアプローチも、非リレーショナル データベースに当てはめるのは困難です。
DatastoreはRDBの代わりにはならない。同じ考えでやることは困難のようです。
具体例:マルチユーザーブログ
User
Id | username | name |
---|---|---|
1 | tonystark | Tony Stark |
2 | dianaprince | Diana Prince |
Post
Id | user_id | content |
---|---|---|
1 | 1 | Hello, world! |
2 | 2 | Another post |
特定ユーザーの検索クエリ
user_idが1のユーザーの投稿で検索する
SELECT * FROM Post WHERE user_id = 1
Datastoreのモデリング
Key | Data |
---|---|
(Post, 1) | {“user”: Key(User, tonystark), “content”: “Hello, World!”} |
(Post, 2) | {“user”: Key(User, dianaprince), “content”: “Another post”} |
(User, tonystark) | {“name”: “Tony Stark”} |
(User, dianaprince) | {“name”: “Diana Prince”} |
Entityが特定のKindを持っている構造になります。今回の例だと、(Post, 1)
PostがKindに当たります。RDBでいうところの、Rowがそれぞれ特定のTableを持っているという構造になります。
では、Post Kindでusernameをフィルタリングすると以下のようなクエリになります。
tonystarkでフィルダリングする例
datastore.Query(kind='Post', filters=[('user', '=', Key('User', tonystark))])
上記のようなグローバルクエリをDatastoreで実行すると、結果整合性を持つことになります。
結果整合性とは
NoSQLでよく登場する性質で、「データの何らかの変更は最終的には全体に適用される」というものです。変更の定義は、追加や削除、更新を総称しているものとします。これはどういうことかと言うと、様々なユーザーがリソースに対して操作を行ったりします。場合によっては、作業が衝突して問題が起きそうですが、これは最後に行った更新を適用するようにしていて、結果的には問題ないとするものです。
つまり、様々なCRUDの操作があったとして、
時間 | 操作 |
---|---|
12:10 | ☆更新 |
12:12 | ◯更新 |
12:09 | △更新 |
上記のような操作のログデータが集約された時に、最終的に適用されるのは、最終更新時間である「◯更新」が正しいとします。なので、基本的にひたすらInsertをしていくということになります。(帳簿的な感じ?)
RDBにおいては厳密な一貫性を重視しているということから、性質上全く違うということがここから分かります。
ちなみに、Datastoreでは、データ整理方法に応じて強整合性と結果整合性のどちらかを選択出来ます。
強い整合性
強い整合性は、先程の結果整合性と処理の流れそのものは同じですが、データに対してアクセス出来るタイミングが全く異なります。分散させたストアにそれぞれ適用が完了してから、ロックを解除します。これらのバランスや性質については公式ドキュメントが非常に分かりやすくまとめてくれているので、参照してみてください。
KindやKeyの性質を理解する
先程紹介した記事には以下のように書かれています。
カインドをテーブルと、キー名をプライマリ キーと考えたくなるかもしれません。しかし、キーは祖先を持つことができ、そのためにキーパスが存在します。非リレーショナル データをテーブルやキーの観点からとらえるのではなく、ファイル システムとして考えてみましょう。
ファイルシステムとして考える???
対象
Key | Data |
---|---|
(Post, 1) | {“user”: Key(User, tonystark), “content”: “Hello, World!”} |
(Post, 2) | {“user”: Key(User, dianaprince), “content”: “Another post”} |
(User, tonystark) | {“name”: “Tony Stark”} |
(User, dianaprince) | {“name”: “Diana Prince”} |
ファイルシステムに置き換える
/1.post /2.post /tonystark.user /dianaprince.user
ユーザー別にグループ化する
/tonystark.user /1.post /dianaprince.user /2.post
最初の投稿パスは、/tonystark.user/1.post
となります。つまり、以下のように整理されています。
Key(ファイルパス) | Data(ファイル) |
---|---|
(User, tonystark) | {“name”: “Tony Stark”} |
(User, tonystark, Post, 1) | {“content”: “Hello, World!”} |
(User, dianaprince) | {“name”: “Diana Prince”} |
(User, dianaprince, Post, 2) | {“content”: “Another post”} |
投稿をユーザー別に整理すると、エンティティ グループが作成されます。その中にはユーザーのプロフィールとすべての投稿が含まれます。Cloud Datastore では、1つのエンティティ グループに対するクエリは強い整合性を持ちます。このため、ユーザーは投稿を作成すると、すぐに見ることができます。祖先をキーに指定してクエリを行うことも可能です。
datastore.Query(kind='Post', ancestor=Key('User', username))
このようなパターンの場合、強い整合性を持つとあります。つまり、その分のトレードオフが発生することになります。Datastoreでは、各エンティティグループには、約1秒間に1回しか更新できません。その代わり、全ての読み取りが強い整合性を持ちます。ここでの更新はCRUD全般を指します。 ※1秒間に1回というのは、各エンティティグループにのみ適用されるルールで、別エンティティグループなら同時操作を可能としています。
この設計の場合、全てのユーザがアップロードした投稿を返すクエリは結果整合性を持つようになります。
datastore.Query(kind='Post')
つまり、以下のようになります。
クエリ | 性質 |
---|---|
全ての投稿を取得 | 結果整合性 |
自分の投稿を取得 | 強い整合性 |
自分の投稿だけを見る分にはすぐ確認できますが、投稿が全体に公開されるフィードなどにはすぐ更新されませんということだと思います。ここまでの説明で分かる通り、結果整合性と強い整合性は全く違う性質を持っているので、使い所をしっかり判断することが大事だと考えられます。
具体例:Wiki
ファイル パスのメタファーを使って、シンプルな Wiki をモデリングすることもできます。Wiki では、ページを保存するたびに新しいリビジョンが作成されます。ユーザーは任意のリビジョンからページをリストアできます。 このデータをファイル システムとして表現すると、次のようになります。
この構造は、各ページのすべてのリビジョンを別々のエンティティ グループに保存します。
/home.page / current.revision / 05-29-2015-10-30-27.revision / 05-20-2015-06-33-11.revision /another.page / current.revision / 04-10-2015-11-23-10.revision
データの操作の流れは以下の通りです。
現在のページデータ(current.revision)を新しいリビジョンにコピーし、current.revisionを新しいコンテンツで上書きします。Datastoreにあるトランザクションを利用するとこういった複数回の処理を、整合性を保つことが出来ます。つまり、成功すれば適用し、失敗すればロールバックしてくれるということです(すごい)
このトランザクション処理は、25のエンティティグループにまたがって実行することが出来ます。ですが、このトランザクションは範囲が多ければ多いほど、失敗の確率が上がるので注意が必要そうです。
これら関連するリビジョンをリストで取得することが、祖先キーを指定することで可能となっています。
datastore.Query(kind='revision', ancestor=Key('page', home))
リビジョンの更新はほとんど同じで、新しく投稿されたコンテンツを使うのではなく、current.revisionを更新することで実現出来ます。
まとめ
記事には以下のようにまとめられていました。
- ページは 1 秒間に 1 回まで更新できる。
- ページ上のクエリとそのリビジョンは強い整合性を持つ。
- 保存操作では、強い整合性を持つクエリが使用されるため、ページの保存やリビジョンのリストアは、トランザクションとして実行できる。
- ページ数が膨大でも、ページの読み書きは高速。
これは理解するためには、まずは使ってみて、経験を積むことが大事な気がしますね。なんか作ったらまとめてみようと思います。