Google Cloud DatastoreをGoogle App Engineから利用する

前提知識はProgateでのPythonレッスン修了レベルです。Pythonで説明しますが、他の言語でも読み替えられると思います。

Google Cloud Datastoreとは

Google Cloud Platformの誇る最先端のデータベースです。
Google Cloud Datastore の概要  |  Google Cloud

これまでデータベースといえばSQL(Progateでも習えますね)でしたが、今はNoSQLといわれる、つまりSQLでないデータベースがあります。SQLと違ってAPI的に簡単に扱えるのがメリットです。

ちなみにGoogle Cloud PlatformにはNoSQL・SQLともにいろいろな種類があります。

公式ページに比較があります。想定される用途なども書いてあるので、これを見て選べばいいでしょう。
ストレージ オプションの選択  |  Google Cloud

ユーザーデータなどのシンプルで低容量のデータベースにはGoogle Cloud Datastoreがぴったりでしょう。

構造

f:id:simonsnote:20180531150337p:plain:w600

Key-Value型というらしいです。データベース全体を辞書型と思えばわかりやすいですね。

Propertyはいくつでも作れます。また、ひとつのエンティティの中で、ひとつのPropertyに対して複数のValueを持てます。しかもそれらは同じ型である必要はありません。

Propertyに設定できる型

文字列、数値、他のEntityへのリンクをはじめとしていろいろな型を利用できます。こちらの公式ドキュメントにリストがあります。
Entity Property Reference  |  App Engine standard environment for Python  |  Google Cloud

Keyだけは文字列(string)か整数(integer)である必要があります。

また、当然ながら「key」という名前のPropertyをつくることはできません。

また__(アンダースコア2つ)ではじまるProperty名はシステムに予約されているので自分でつけることはできません。

GAEからGoogle Cloud Datastoreを利用する方法

Google App EngineではデフォルトのデータベースとしてGoogle Cloud Datastoreを利用できるようになっています。

同じGCPアカウント間でのやりとりなので認証が不要です。ライブラリに定義されているメソッドを実行するだけでOK。これはかなり楽ですね。

import

ライブラリ(パッケージ)は前回の手順でセットアップしていればあとはimportするだけです。

from google.appengine.ext import ndb

なおndbの他にdbというライブラリもありますが、ndbの方が新しいライブラリになります。いまからわざわざdbを使い始めるメリットはありません。
ndbdbの対照表はこちらをご覧ください。

データベースへのアクセス

公式リファレンスはこちらです。
Creating, Retrieving, Updating, and Deleting Entities  |  App Engine standard environment for Python  |  Google Cloud

新しいデータを格納する

以下のようなステップになります。

  1. データベースを表すクラスを作成する(クラス名=Kindとなる)
  2. 作成したクラスのインスタンスを作成する(インスタンス=Entityを示す)
  3. インスタンス変数(=Property)に値(Value)を入れる
  4. 作成したインスタンスをGoogle Cloud Datastoreへ送信する

まずデータベースそのものをあらわすクラスを定義しましょう。クラス=kindとなります。
ndbライブラリに定義されているModelクラスを継承します。今回は「People」というkindを作ってみましょう。

class People(ndb.Model):

そうしたら、その際にPropertyも同時に定義します。今回は名前と生年月日をPropertyとして保存できるようにしてみましょう。
Propertyにはそれぞれ型を指定します。今回は、

  • name:文字列(str)型
  • birthday:日付(datetime.date)型

というようにします。型の指定にはライブラリに定義されたメソッドを使います。指定したい型ごとにメソッドが決まっているので、こちらのリファレンスから選んで使いましょう。

class People(ndb.Model):
    name = ndb.StringProperty()
    birthday = ndb.DateProperty()

これで、こんなイメージのクラスができあがりました。
f:id:simonsnote:20180531165923p:plain:w400

そうしたらEntityをつくりましょう。インスタンスがEntityそのものとなります。

taro = People(name='太郎', birthday=datetime.date(1956, 7, 3))
yoshiko = People(name='よし子', birthday=datetime.date(1923, 1, 1))

あとは作成したインスタンスをGoogle Cloud Datastoreに送信すれば完了です。
これはput()メソッドひとつで完了。ちょう楽。

taro.put()
yoshiko.put()
データベースからデータを取得する(Keyがわかっている場合)

Keyがわかっていればとても簡単にデータベースにアクセスできます。

なお、keyが分かっている場合というのはそんなにないはずです。上でEntityを作成したときにKeyの話がでてきませんでしたね。Entity作成時にKeyを特に指定しなかった場合、Keyは自動で生成されます。Keyを指定してEntityを作成することもできるのですが、よほど自信があるのでなければおすすめしません。なぜならKeyは完全にユニークである必要があり、また変更できないからです。

Keyがわかっていればデータの取得はとても簡単です。

michael = キー.get()

これで、変数michaelはEntityそのものを表すインスタンスになりました。

PropertyのValueを取得してみましょう。

print( michael.name )
#出力結果:マイケル

michaelの名前を変更してデータベースを更新してみましょう。

michael.name = 'マイケール'
michael.put()
データベースからデータを取得する(Keyがわからない場合)

Keyが分かっていない場合は3段階の手順が必要です。

  1. クエリを作成する
  2. フィルタでクエリの範囲を絞り込む
  3. 作成したクエリを.get()もしくは.run()する

まずはクエリを作成します。クエリとは「こんな性質のEntity」というイメージです。作成したクエリを.get()or.run()すれば、該当するEntityがGoogle Cloud Datastoreから返されます。

query_for_taro = People.query(People.name == '太郎')

これでKindPeopleのEntityのうち、name太郎である集団のイメージができました。

今回は太郎は一人しかいない自信があります。一発で目的のEntityを掴めるという自信があるなら、次のフィルタは特に必要ありません。

もし太郎が複数存在し、今回はbirthday1956-7-3太郎だけが欲しいような場合は、さらにフィルタをします。

query_for_taro = query_for_taro.filter(People.birthday == datetime.date(1956, 7, 3))

ちなみに比較演算子は==以外も利用できますし、ANDORのような表現も利用できます。詳しくはこちらのドキュメントをご覧ください。

クエリを作成できたら、それをGoogle Cloud Datastoreに送信して該当するEntityを受け取ります。
このときに.get()メソッドか.run()メソッドのどちらかを利用します。

.get()メソッドは該当するEntityのうち、最初のひとつのみを返します。

taro = query_for_taro.get()

つまり、返されるのはEntityそのものを表すインスタンスです。(対象のEntityがない場合はNone(PythonではnullではなくNoneといいます)が返ります)

.run()メソッドは該当するEntityすべてをリスト(Pythonでは配列ではなくリストと呼びます)で返します。

list_of_taros = query_for_taro.run()

つまりこういうことです。

query_for_taro.get() == query_for_taro.run()[0]
#この式は常にTrue

.run()メソッドは引数をつけなければ該当するEntityをすべて返します。返す数の上限を設定するには引数を利用します。

list_of_taros = query_for_taro.run(limit=10)
#list_of_tarosには最大で10個しか入らない

ちなみに、.run()とよく似た.fetch()というメソッドもあります。

違いはキャッシングのみです.fetch()はクエリに結果をキャッシュさせます)。Googleでは特に理由のない場合は.run()を推奨しています。

EntityのPropertyを更新する

Propertyを更新するには、取得・または作成したEntityオブジェクトのインスタンス変数を変更して.put()すればOKです。

class People(ndb.Model):
    name = ndb.StringProperty()

#taroはKindがPeopleでnameが'太郎'のEntity
taro = People.query(People.name == '太郎').get()

#taroのnameを'三郎'に変更
taro.name = '三郎'

#変更を反映
taro.put()

以下の方法も利用できます。変数でPropertyを指定したいような場合に便利です。

#taroはKindがPeopleで'name'が'太郎'のEntity
taro = People.query(People._properties['name'] == '太郎').get()

NDB Queries  |  App Engine standard environment for Python  |  Google Cloud

データベースからEntityを削除する(Keyが必要)

データベースからEntityを削除するにはKeyが必要です。Keyの取得方法はあとで説明します。

ちなみにEntity自体は削除せずいずれかのValueのみ削除するならデータベースからデータを取得するの応用でできますね。インスタンスの特定のPropertyにNoneを代入して.put()すればいいのです。

Entity自体を削除するには.delete()メソッドを使います。

taro.キー.delete()

ちなみに.delete()メソッドの返り値は常にNoneです。

その他のオペレーション

ここから下は必要な時にリファレンスがわりにみてもらえればいいです。ここまでの内容でもう十分に利用できます。

Keyの取得

実はkeyとはKeyクラスのインスタンスです。

Keyクラスは最低2つのインスタンス変数を持ちます。

Key(kind, id)

kindにはそのEntityが所属するKindが入ります。ここまでの文脈では「Key」という言葉を「Entity固有の文字列か整数」と説明してましたが、これはidのことです。

.get()などでEntityを示すインスタンス(Modelクラスのインスタンス)を取得できていれば、インスタンス変数.keyでkeyを取得できます。

キー = インスタンス.key

また、クラス名(kind)やidも取得できます。

クラス = インスタンス.key.kind()
id = インスタンス.key.id()

Keyクラスについて詳しくはこちらのリファレンスをご覧ください。
dbライブラリのリファレンスなので注意してください。このページの最後にndbdbライブラリ対照表へのリンクを張っているので、必ずそこでndbライブラリに変換して利用してください。

他のEntityへのリンク

Kindが異なる場合を含め、他のEntityへのリンクをPropertyに保有できます。Modelクラスのインスタンス(kindを示すインスタンス)を定義する際に.KeyProperty(kind)メソッドでインスタンス変数を定義しておきましょう。

class People(ndb.Model):
    film_key = ndb.KeyProperty(Films)

このときfilm_keyはKindがFilmsのEntityのKeyです。

例えば以下のコードではEntitytaroからEntitydeath_in_Veniceの特定のPropertyを取得することができています。

例:太郎の好きな映画の監督の名前を表示したい

class Films(ndb.Model):
    director_name = ndb.StringProperty()

class People(ndb.Model):
    name = ndb.StringProperty()
    favorite_film_key = ndb.KeyProperty(Films)


death_in_Venice = Films(director_name='Visconti')

taro = People(name='太郎', favorite_film_key=death_in_Venice.key)


death_in_Venice.put()
taro.put()


#↑このような状態になっているとして


taros_favorite_film = taro.favorite_film_key.get()
#taro.favorite_film_key == death_in_Venice.key なので、taro.favorite_film_key.get() == death_in_Venice

print( taros_favorite_film.director_name )
#出力結果:Visconti

実は、上のfavorite_film_keyにあたる専用の変数が最初から用意されています。Modelクラスにはparentという引数が用意されているので、以下のようにいきなり指定してしまえば大丈夫です。

class Films(ndb.Model):
    director_name = ndb.StringProperty()

class People(ndb.Model):
    name = ndb.StringProperty()


death_in_Venice = Films(director_name='Visconti')

taro = People(name='太郎', parent=death_in_Venice.key)

death_in_VeniceparentにもつEntityを選ぶには.query()メソッドにancestorという引数を入れます。

query = People.query(ancestor=key_of_death_in_Venice)

NDB Queries  |  App Engine standard environment for Python  |  Google Cloud

KindのEntityをすべて取得する

.query()メソッドに引数をいれなければKindに所属するすべてのEntityが含まれます。

all_people = People.query().fetch()
#run()は利用できないようです
Entityの順番の指定

クエリは条件にあてはまるすべてのEntityを含みます。なのでその中で順番を指定したいこともありますね。

順番の指定は.order()メソッドを使います。Propertyごとに昇順(asc)・降順(desc)で指定できます。

例えばkindがPeopleのEntityを全て取得したいが、その順番は

  1. まずnameについて昇順
  2. nameが同じ場合はbirthdayについて降順で並べる
という場合は以下のようにします。

query = People.query().order(People.name, -People.birthday)

降順の場合は-(マイナス)をつけるわけですね。

特定のPropertyのみ取得する

これまで扱ってきたクエリやフィルタでは、「あるPropertyについて特定のvalueをもつEntity」という絞り込み方をしてきました。

つまり、例えばPeopleのうちnamebirthdayについてのみ興味がある場合でも、それ以外のfavorite_filmとかageとかのPropertyも含んだ結果が返されます。

もちろんだからといって困ることはないですね。しかし不要なPropertyは最初から除外してもらうことでパフォーマンスが向上(値が返されるまでの時間の短縮、Google Cloud Datastoreの使用料の節約)します。

これはProjection Queriesという機能で実現できます。
.run().fetch()メソッドにはprojectionという引数があるので、ここに指定したいPropertyをいれればOKです。

list  = People.query().run(projection=[People.name, People.birthday])

projectionはリストです。ひとつでもprojection=[People.name]のようにします。

ちなみに以下のような書き方も可能です。

list = People.query().run(projection=["name", "birthday"])

Projection Queriesについて詳しくはこちらのリファレンスをご覧ください。

リストPropertyに特定の値が含まれるEntityをfetchする

comingsoon
NDB Queries  |  App Engine standard environment for Python  |  Google Cloud

Expandoクラス

これまでKindを示すクラスはModelクラスを継承して作成していました。実はもうひとつExpandoというクラスがあります。

ExpandoクラスはModelクラスの子クラスです。

Modelクラスを継承して作成したクラスのインスタンス変数はすべて固定Property(Fixed Property)でした。つまり、作成したPropertyに入るvalueの型は固定で、またクラス作成時に定義しなかったインスタンス変数は利用できません。

class People(ndb.Model):
    name = ndb.StringProperty() #nameにはstring型しか入れられない


taro = People(name=['Tanaka', 'Taro']) #エラー:nameにはlist型は入れられない。

yoshiko = People(age=23) #エラー:インスタンス変数ageは未定義

Expandoクラスを利用すると、上の2つとも可能になります。つまりクラス作成時にインスタンス変数を定義する必要がなく、後から自由な型でインスタンス変数を作成できます。

class People(ndb.Expando):
    pass


taro = People(name=['Tanaka', 'Taro']) #list型の変数nameを作成

yoshiko = People(age=23) #integer型の変数ageを作成

yoshiko.favorite_food = 'ラーメン' #yoshikoの新しいPropertyとしてfavorite_foodを追加

taro.favorite_food = ['カレー', 'パッタイ'] #同じ名前のPropertyでも異なる型を利用可能

このような動的なPropertyをDynamic Propertyといいます。Dynamic PropertyはExpandoクラスのインスタンスでしか利用できません。

それぞれのDynamic Propertyはvalueが代入されたタイミングで新たに生成され、それまでは存在しません。

Dynamic Propertyは削除できます。

del taro.favorite_food

ExpandoクラスでもModelクラス同様にFixed Propertyも定義できます。

class People(ndb.Expando):
    age = ndb.IntegerProperty()


taro = People(age=67, name='Taro') #Fixed Propertyであるageにvalueを代入しつつDynamic Propertyとしてnameを作成

yoshiko = People(age='23歳') #エラー:ageはFixed Propertyとしてinteger型で定義しているので、string型は入れられない

del taro.age #エラー:Fixed Propertyは削除できない

Expandoクラスについて詳しくはこちらのリファレンスをご覧ください。
dbライブラリのリファレンスなので注意してください。このページの最後にndbdbライブラリ対照表へのリンクを張っているので、必ずそこでndbライブラリに変換して利用してください。

その他・詳細

クエリの正体はQueryクラスのインスタンスです。

クエリに対して利用可能なメソッド(.get()など)の詳細はこちらのリファレンスをご覧ください。
dbライブラリのリファレンスなので注意してください。このページの最後にndbdbライブラリ対照表へのリンクを張っているので、必ずそこでndbライブラリに変換して利用してください。

デプロイ

アプリケーションのデプロイの際に、index.yamlファイルを一緒にアップロードする必要があります。これがない場合、存在しないKindに対して.query()をかけるとエラーになります。

なお、Admin Server (dev_appserver.py app.yamlで起動するローカルの開発環境) ではこの作業は必要なく、index.yamlはそれぞれのKindに対しての初回の.query()時に自動で生成・更新されます。

このAdmin Serverで自動生成されたindex.yamlをそのままアップロードすることもできますし、手動でindex.yamlを編集することもできます。

いずれにしろ作成したindex.yamlをデプロイ(実際に稼働するアプリケーションにアップロード)するには、ローカルのアプリケーションフォルダの直下(dev_appserver.py app.yamlを行うのと同じ場所)で以下のコマンドを入力します。

gcloud datastore create-indexes index.yaml

なおDatastore Indexesについてはこちらが公式リファレンスになります。手動でindex.yamlを設定する場合はこちらを参照してください。
インデックス設定  |  Cloud Datastore ドキュメント  |  Google Cloud
gcloud ツールを使用したアプリケーション テストとインデックス管理  |  Cloud Datastore ドキュメント  |  Google Cloud

補足:リファレンスについて

2018/6/1現在、Google Cloud Datastoreの公式ガイドはほぼ英語です(概要のページなど一部は日本語もあり)

Cloud Datastoreのガイドやドキュメントは正直みにくいです。ライブラリはndbが新しい方でdbは古い方です。それぞれドキュメントも別になっているのでndbをの方をみようとするのですが、実はdbから変更のない部分については書いていなかったりします。ややこしい。

なので公式ドキュメントをみる際はndbのページを中心に、書いていないことがあればdbを参照しつつ対照表でndbに変換してください。めんどいですね。

左側のグローバルメニューに全てのページが並んでいるので、根気強く探してください。基本的には下に貼ったページより下の階層に全部あるはずです。

リンクはPython用ですが、ページ上部に他の言語への切り替えボタンがあるはずです。