Cloud Datastoreで強整合性を実現する

旧来のDatastoreのクエリは基本的に結果整合性なので、更新した直後にクエリを投げると更新前の結果(つまり最新ではない結果)が返ってきたりする。

確実に最新の結果を得たければ強整合性を持ったクエリを使う。
旧来のDatastoreにおいて強整合性を持ったクエリとはだいたい祖先クエリ(アンセスタークエリ)とイコールになる。

しかし祖先クエリを使うのはエンティティグループの設計などをしっかりしないといけないので大変である。
Firestoreへの自動アップグレードが簡単にできるようなので、結果整合性に悩まされているようならFirestoreのDatastoreモードにアップグレードしてしまうのが手っ取り早そう。アップグレードすれば強整合性がデフォになる。

結論: FirestoreのDatastoreモードにアップグレード

ちなみに上記のドキュメントに

Google Cloud は 2021 年から、既存の Cloud Datastore データベースを Datastore モードの Firestore に段階的にアップグレードしていく予定です。

と書いてあるのでほっとけば強制的にアップグレードされてしまうのかな?

情報の錯綜に注意

Datastoreの強整合性について調べているとけっこう公式のドキュメントを読んでいるはずなのに矛盾したことが書いてあったりして混乱することがあった。

  • NDBに古いApp Engine NDBと新しいGoogle Cloud NDBがある
  • Datastoreに旧来のDatastoreとCloud FirestoreのDatastoreモードがある

自分が使っている/使おうとしているのがどの組み合わせなのかをしっかり把握しておき、読んでいる情報がどの組み合わせのものなのかを念頭に置く必要がある。

古いアプリだとApp Engine NDBと旧来のDatastoreの組み合わせになっていることが多いだろう。
今から作るアプリだと自然とGoogle Cloud NDBとCloud Firestoreになるはず。

祖先クエリ

一応祖先クエリについてメモ。
強整合性のためにはCloud FirestoreのDatastoreモードを使う方がいいので要る情報か分からないけど。

強整合性に対応するデータ構造に以下の一文がある。

強整合性を実現する場合は、祖先パスを持つエンティティを作成する方法がより効果的です。
祖先パスは、作成された各エンティティがグループ化される共通のルート エンティティを識別します。
この例では、種類が TaskList、名前が default となる祖先パスを使用します。

共通の親(祖先パス)を持つエンティティグループを作成し、クエリの際に親を指定することでそのエンティティグループ内では強整合性が実現されると言うこと。

コードは以下のようになる。

ancestor = client.key("TaskList", "default")
query = client.query(kind="Task", ancestor=ancestor)

旧来のDatastoreでは1つのエンティティグループに対して1秒につき1トランザクションという書き込みスループットの制限が課される。
親子関係があるからとなんでも祖先パスを設定するとまともに動かないアプリができたりする危険性がある。

それにしても強整合性を持つクエリのために親子関係が必要ないエンティティでも親を作って指定しないといけないというのは微妙だ。
設計段階でエンティティグループが前提となっていないと後から組み込むのもなかなか難しい。

試行コード

Cloud FirestoreのDatastoreモードにて結果整合性と強整合性の違いを見るために以下のようなコードを書いてみたのだが・・・

after_t = key.get()
# after_t = key.get(read_consistency=ndb.EVENTUAL)

キーからのルックアップは強整合性になるが、オプションで結果整合性も指定できる。
しかしオプションを指定しても最新の更新結果が返ってきたので検証にならず。データ量が少なすぎて遅れる余地がないのかも。

一応main.py全コード。

from flask import Flask

from google.cloud import ndb

import datetime
import time

class TestModel(ndb.Model):
    name = ndb.StringProperty()
    updated = ndb.DateTimeProperty()

app = Flask(__name__)

@app.route('/')
def hello():
    client = ndb.Client()

    with client.context() as context:
        t = TestModel(name="THIS IS TEST", updated=datetime.datetime.now())
        key = t.put()
        print(key)

        # key = ndb.Key("TestModel", key.id())
        before_t = key.get()

        time.sleep(1)

        t.updated = datetime.datetime.now()
        t.put()

        after_t = key.get()
        # after_t = key.get(read_consistency=ndb.EVENTUAL)

        output = t.name + " / " + " before:" + before_t.strftime("%c") + ", after" + after_t.updated.strftime("%c")

    return output


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080, debug=True)

参考

旧来のものの情報

FirestoreのネイティブモードとDatastoreモードについて

移行案件でなければネイティブモードを選んでおくのが丸い。