Google Cloud Datastoreを利用したサーバーをFlaskでつくる [GAE]

前回はじめてのFlaskアプリをGoogle App Engineで作ったわけですが、今回はデータベースを利用したアプリを作ります。

HTTPリクエストを受けて値を保存したり返したりするRESTサーバーをGoogle Cloud Datastoreで作ってみましょう。
▷HTTPリクエストの構造については知っているものとします。

前回同様、使うのはFlask。Google App Engine(以下GAE)でホストしています。

Google Cloud Datastoreとは

Google Cloud Platformの誇る最先端のデータベース(NoSQL)です。GAEからの利用方法はまとめてあるので、随時参照してください。
Google Cloud DatastoreをGoogle App Engineから利用する

基本的には上のページを読んでいなくてもわかるように書きます。詳細を知りたいときだけ上のページをリファレンスがわりに参照してもらえれば大丈夫と思います。

ちなみにGoogle Cloud Datastoreにおける各名称の意味はこのようになっています。

f:id:simonsnote:20180531150337p:plain:w500

Google Cloud Platformでの準備

Google Cloud DatastoreはGCPのデフォルトのデータベースなので、特になにか設定する必要はありません。いきなり使えます。

GCPでは「プロジェクト」という単位で各サービスを管理します。同じプロジェクトの中ではCompute EngineもApp EngineもCloud Datastoreも最初から結びついているのです。なのでたとえば同じプロジェクト内でCompute EngineとApp Engineでそれぞれ関係ないアプリを運用するというのはあまりよくないですね。(データベースを共有するなどであればいいですが)

アプリをつくっていく

さて、今回は前回のFlaskアプリをベースに、Cloud Datastoreにユーザー情報を保存・取り出すサーバーを作ります。
※なお当然ながら今回つくるサーバーはテスト用でセキュリティもなにもあったもんじゃないので、実際のユーザー情報をいれてはなりません。(それは次回やりましょう)

ここからは前回作った最小限のFlaskアプリにコードを書き足す形で作っていきます。

今回はサーバーということで、http POSTでユーザー情報を保存・更新・取り出すアプリを作ります。
取り出す際もGETでなくPOSTなのは、リクエストにパスワードを含めてもらうことで第三者がデータを取り出せないようにするためです。パスワードはGETで扱ってはなりません(GETリクエストの内容は公開情報であることが前提となっています)。

今回コードを追加するのはmain.pyのみです。

前回作ったmain.py

さて、前回作ったmain.pyの内容は以下のようになっています。

# coding: UTF-8
import flask

app = flask.Flask(__name__)


@app.route('/')
def toppage():
    return flask.render_template('toppage.html')

@app.route('/showname')
def showname():
    name = flask.request.args['name']
    return flask.render_template('showname.html', name=name)

前回までのコードを消す必要はありません。新たにデータベースと情報をやりとりするためのページを作りましょう。

ライブラリをimportする

Google App Engineでデフォルトで用意されているndbライブラリをインポートするだけです。

from google.appengine.ext import ndb

これだけでライブラリが利用可能になります。

データベースを定義する

まずはデータベースに保存するデータのクラスを作成します。

  • クラス名=kind
  • インスタンス変数=propertyの値(value)

となります。

クラスはndbライブラリに定義されているModelクラスを継承して作成します。

今回はPropertyには以下の3つを用意します。

  • name(string型)
  • mail(string型)
  • password(string型)

Propertyを用意するには専用のメソッドを使用します。今回は全てstring型ですが、型ごとにメソッドが用意されています。

class User(ndb.Model):
    name = ndb.StringProperty()
    mail = ndb.StringProperty()
    password = ndb.StringProperty()

あとは今作成したUserクラスのインスタンスを作成すればそれがEntityになります。インスタンス変数に値を代入すればそれぞれのPropertyにvalueを入れたことになります。

パスを定義する

Cloud Datastoreと情報をやりとりするには、用意したパスにPOSTリクエストを送ってもらう形になります。

前回まではルーティングは@app.route(パス)の形でしたが、実は.route()メソッドには引数でHTTPメソッドを指定することができます。

今回は

  • /database/newPOSTリクエストを送信した際に新しいデータの保存
  • /database/getPOSTリクエストを送信した際にデータの取り出し
  • /database/refleshPOSTリクエストを送信した際にデータの更新

をすることにしましょう。

.route()メソッドの引数methodsに許可するリクエストメソッドをリスト型で入れます。

新しいデータの保存

/database/newPOSTリクエストを送信した際に新しいデータの保存を行います。

イメージとしてはこんなかんじですね。(ここではエスケープやエンコード・デコードは考えません)

POSTリクエストのボディ:name=フラスク太郎&mail=abc@example.com&password=aabbcc

この形を「application/x-www-form-urlencoded」といいます。HTTPリクエストでは最もメジャーな形です。パラメータ1=値1&パラメータ2=値2...というようにパラメータ=値&で連結します。パラメータのことをid、値のことをdataといいます。

さて、flask.requestはリクエストボディのパラメータを.formに辞書型で保有しています。パラメータにアクセスするにはflask.request.form[キー]だけでいいのです。これだけでPOSTリクエストを扱えてしまうのだから便利ですね。

@app.route('/database/new', methods=['POST'])
def database_new():
    name = flask.request.form['name']
    mail = flask.request.form['mail']
    password = flask.request.form['password']

Userクラスのインスタンスを作成し、インスタンス変数に今取得した値をいれましょう。

user = User(name=name, mail=mail, password=password)

そうしたらあとは作成したインスタンスuserをDatastoreに送信するだけです。

これには.put()メソッドを利用します。

user.put()

これで新しい値の保存は完了です。

データの取り出し

データの取り出しはUserクラスに対して.get()メソッドを使います。

今回はリクエストで受け取ったユーザーネーム(nameパラメータ)とパスワード(passwordパラメータ)が正しい場合にそのユーザーのメールアドレス(mailProperty)を返し、正しくない場合にはエラーメッセージを返しましょう。

まずはリクエストからユーザーネームとパスワードを取得します。

@app.route('/database/get', methods=['POST'])
def database_get():
    name = flask.request.form['name']
    password = flask.request.form['password']

そうしたら、まずはそのユーザーネームとパスワードの組み合わせがデータベースに存在するか確認します。

手順は以下のようになります。

  1. クエリ(指定したnamepasswordを持つEntityを要求するリクエスト)を作成する
  2. 作成したクエリを.get()する

渡したユーザーネームとパスワードの組み合わせがデータベース(Kind=User)に存在すれば、有効なEntityが返ってきます。なければNoneが返ってきます。

ここからif文で

  1. 有効なEntityが返ってきたら(Noneでなかったら)→そのEntityのmailPropertyを出力
  2. Noneだったら→エラーメッセージを出力

すればOKです。

まずはクエリを作成しましょう。クエリの作成には.query()メソッドを使います。引数にPropertyと値の評価式を入れます。今回はANDを使います。

query = User.query(ndb.AND(User.name == name, User.password == password))

そうしたらこのクエリをDatastoreに送信し、返り値を変数に入れましょう。

result = query.get()

返ってきた値には有効なEntityかNone(該当するEntityがなかった場合)のどちらかが入っているはずです。if文で条件分岐しましょう。

resultが有効なEntityだった場合は、そのmailPropertyをreturnします。Propertyはインスタンス変数として保有されています。

if result is None:
    return 'ユーザーネームとパスワードの組み合わせが正しくありません'
else:
    return result.mail

これでOKです。

データの更新

データを更新するには.get()で取得したEntityのインスタンス変数(=Property)を新しく代入して.put()でDatastoreに戻せばOKです。

今回は以下のような仕様にしましょう。

  • /database/refleshPOSTリクエストを送信
  • リクエストボディはuser=変更したいユーザーのユーザー名&変更したいパラメータ1=新しい値&変更したいパラメータ2=新しい値...
flask.request.formは辞書型なので、Pythonの.items()メソッドを使って変更したいパラメータ全てについてfor文で処理をしましょう。インスタンス変数へのアクセスはPythonのsetattr(インスタンス, 属性名, 値)関数を使います。

@app.route('/database/reflesh', methods=['POST'])
def database_reflesh():
    username = flask.request.form['user']
    user_entity = User.query(User.name == username).get()

    #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新
    if user_entity:
        for key, value in flask.request.form.items():
            if key != 'user':
                setattr(user_entity, key, value)

        user_entity.put()
    else:
        return '該当するユーザーがみつかりませんでした'

これでもいいのですが、定義していない属性(Property)がリクエストに含まれていた場合はエラーになります(今回のUserクラスはModelクラスを継承しているので新しいPropertyが追加されてしまうことはありません)。

この問題を解消するにはPythonのhasattr(インスタンス, 属性名)関数を使えばいいですね。Propertyが存在する場合はTrueが返ります。

@app.route('/database/reflesh', methods=['POST'])
def database_reflesh():
    username = flask.request.form['user']
    user_entity = User.query(User.name == username).get()

    #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新
    if user_entity:
        for key, value in flask.request.form.items():
            if key != 'user':
                if hasattr(user_entity, key):
                    setattr(user_entity, key, value)

        user_entity.put()
    else:
        return '該当するユーザーがみつかりませんでした'

これでOKです。

出来上がったコード

これでmain.pyは完成です。

出来上がったmain.pyがこちら。前回のコードに今回のコードをくっつけただけです。前回のコード(ルーティング部分)は別に消しても構いません。

# coding: UTF-8
import flask
from google.appengine.ext import ndb

app = flask.Flask(__name__)

class User(ndb.Model):
    name = ndb.StringProperty()
    mail = ndb.StringProperty()
    password = ndb.StringProperty()


@app.route('/')
def toppage():
    return flask.render_template('toppage.html')

@app.route('/showname')
def showname():
    name = flask.request.args['name']
    return flask.render_template('showname.html', name=name)

@app.route('/database/new', methods=['POST'])
def database_new():
    name = flask.request.form['name']
    mail = flask.request.form['mail']
    password = flask.request.form['password']
    user = User(name=name, mail=mail, password=password)
    user.put()

@app.route('/database/get', methods=['POST'])
def database_get():
    name = flask.request.form['name']
    password = flask.request.form['password']
    query = User.query(ndb.AND(User.name == name, User.password == password))
    result = query.get()
    if result is None:
        return 'ユーザーネームとパスワードの組み合わせが正しくありません'
    else:
        return result.mail

@app.route('/database/reflesh', methods=['POST'])
def database_reflesh():
    username = flask.request.form['user']
    user_entity = User.query(User.name == username).get()

    #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新
    if user_entity:
        for key, value in flask.request.form.items():
            if key != 'user':
                if hasattr(user_entity, key):
                    setattr(user_entity, key, value)
        user_entity.put()
    else:
        return '該当するユーザーがみつかりませんでした'

今回は他に変更はないので、あとはデプロイするだけです。

dev_appserver.py app.yamlでテストする場合はlocalhost:8000からDatastoreへの接続をシミュレートできます(Google Cloud SDKの開発機能で、Development app serverといいます)。この環境でテストしたことは実際のDatastoreには反映されません。めちゃめちゃ便利ですね。

テストする

ちゃんと動くかテストしたいのですが、今回作ったサーバーを利用するにはPOSTリクエストを送る必要がありますね。
今回作ったようなサーバーの実際の運用ではJavaScriptなりでPOSTリクエストを送ってくれればいいのですが、ひとまずコマンドラインでPOSTリクエストを送ってみましょう。

以下はMac/Linux向けのコマンドです。Macの場合はターミナルにコードを貼ってEnterを押せばOK。

curl --include --request POST --header 'Content-Type:application/x-www-form-urlencoded' --data 'リクエストボディ' パス

まずは新しいEntityを作成してみましょう。

curl --include --request POST --header 'Content-Type:application/x-www-form-urlencoded' --data 'name=太郎&mail=taro@example.com&password=aabbcc' https://〜〜〜.appspot.com/database/new

返り値を設定していないのでエラーが返ってきますが、POST自体はできています。

GCPコンソールからデータベースをみてみる

さっきのPOSTでCloud Datastoreに新しいEntityができているはずです。GCPのコンソールから確認してみましょう。

GCPの管理画面の左上のメニューから「Datastore」→「Entities」を選びます。

f:id:simonsnote:20180603161711p:plain:w300

Kindを選択する部分があるので、今作成した「User」を選択しましょう。...どうですか?太郎さんはちゃんと追加されていますか?

追加されているのを確認したら、/database/get/database/refleshも試してみましょう。

まとめ

今回、ちゃんと動くFlaskサーバーをGAEで作ることができるようになりました。しかしながらこのままではセキュリティ上、実際の運用はできません。

次回はセキュリティ対策を施し、実際にユーザー情報を格納できるサーバーを作ります。