python×Cloud Functions×FirestoreでAPIを簡単に作ってみる Pt1

# 概要
GCPのCloud Functionsを使って、FirebaseのFirestoreからデータを取得するAPIを作る機会があったのですが、非常に簡単に作れたのでその時やったことをまとめます。
（本当はCloud Functions for Firebaseを使いたかったのですが、慣れているpythonには未対応のため、GCPのCloud Functionsで作ってみることに。）

Pt1では、Firestoreのデータを取得するシンプルなAPIをGCPのコンソールから作成します。
Pt2以降では、ローカルからのテスト、デプロイの方法をまとめます。

## 開発環境・前提
- 開発環境
  - MacOS
  - python3.7
  - pycharm
- 前提知識
  - Firestoreをコンソールから作成済み
  - Cloud FunctionsのHello World関数を作ったことはある

※開発環境はPt2以降で使います。

## プロジェクトの作成
FirebaseまたはGCPのコンソールから画面の指示に従うままに作成できます。
2つの連携について言えば、Firebaseのプロジェクトから先に作成した場合、GCPコンソールの「プロジェクトの選択>すべて」のタブからFirebaseで作成したプロジェクトを選択すれば、連携が完了です。

## Firestoreのデータを取得する関数を書く
前提となるデータベースはこんな感じで、userコレクションからユーザ一覧を取得してみます。
![FireShot Capture 001 - データ – Firestore – sample-project – Google Cloud Platform_ - console.cloud.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/7dca53ad-0c6d-b596-b8fc-9b3db7f6ead3.png)

肝心のソース。とりあえず、Cloud Functionsのコンソールから直接編集してみます。

```python
import json
from google.cloud import firestore


def get_user(request):
    db = firestore.Client()

    docs = db.collection('user').get()
    users_list = []
    for doc in docs:
        users_list.append(doc.to_dict())
    return_json = json.dumps({"users": users_list}, ensure_ascii=False)

    return return_json
```

- 簡単にソースを解説
 - 引数であるrequestはHTTPのパラメータ等を指定しますが、このソースでは使ってません。とりあえず全データ取ってきます
 - userコレクションから取得したdocsをforで回して、docの```to_dict()```メソッドでdictにしています。
 - json.dumpsで```ensure_ascii=False```を指定しないと、日本語のデータが混ざったときにエスケープされた値が返されます。

## デプロイ前の準備
Firestoreからデータを取得するためのパッケージであるgoogle-cloud-firestoreは、Cloud Functionsのデフォルトには含まれません。
そのため、「ソース>requirements.txt」タブから追記する必要があります。

```
# Function dependencies, for example:
# package>=version
google-cloud-firestore==1.2.0
```
Cloud Functionsであらかじめインストールされるパッケージは公式に載ってます。
[Python での依存関係の指定 - あらかじめインストールされるパッケージ](https://cloud.google.com/functions/docs/writing/specifying-dependencies-python?hl=ja#pre-installed_packages)

## デプロイ
Cloud Functionsのコンソールから「実行する関数」に関数名(get_user)を指定してデプロイします。リージョンは「us-central1」がデフォルトですが、詳細オプションから変更可能なので、気になる人は変えましょう。
少し待つとデプロイが完了して、関数が作成されます。
![FireShot Capture 002 - Cloud Functions - sample-project - Google Cloud Platform_ - console.cloud.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/fb3c1f80-3b3b-b03b-a9e4-c01ce35e02dc.png)

## テスト
デプロイした関数をテストしてみます。コンソールからパラメータを指定せずに関数をテストしてみると、userコレクションのデータが取得できます。
![FireShot Capture 003 - Cloud Functions - sample-project - Google Cloud Platform_ - console.cloud.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/abde0d7f-0d9f-1a49-8c16-64e788a61d9c.png)

また、「トリガー」タブからURIが確認できますので、curlでの確認も可能です。

```
$ curl https://<リージョン>-<プロジェクトID>.cloudfunctions.net/getUsers 
{"users": [{"gender": "female", "name": "Alice", "age": 19}, {"age": 21, "gender": "male", "name": "Bob"}]}
```

ひとまず、Firestoreからのデータ取得はできました。
ただ、これだとコレクションの全データを返してしまうので、引数であるrequestを使って取得するデータの条件を指定できるようにします。

## パラメータの条件に一致するデータを取得する

前提として、関数の引数となるrequestは、FlaskのRequestオブジェクトです。
そのため、クラス変数やメソッドはFlaskの公式ドキュメントで確認できます。
[Flask - API](http://flask.pocoo.org/docs/1.0/api/)

例えば、```request.method```と書けばGETやPOSTが文字列として取得できるので、条件分岐に使うことも可能です。

APIでリクエストされたパラメータもこのオブジェクトから取得します。
というわけで早速ソースです。

```python
import json
from google.cloud import firestore


def get_filtered_user(request):
    # リクエスト本文からjsonを取得
    request_json = request.get_json()
    # クエリ文字列を取得
    if request.args and 'name' in request.args:
        request_name = request.args.get('name')
    # jsonから条件を取得
    elif request_json and 'name' in request_json:
        request_name = request_json['name']
    else:
        request_name = ''

    db = firestore.Client()

    # whereでdocument内の条件に一致するデータを取得
    query = db.collection('user').where('name', '==', request_name)
    docs = query.get()
    users_list = []
    for doc in docs:
        users_list.append(doc.to_dict())
    return_json = json.dumps({"users": users_list}, ensure_ascii=False)

    return return_json
```

- コメントが入ってるところが主なソースの修正箇所です。
  - オブジェクトからパラメータを取得
  - リクエストされた内容をwhereで条件にしてFirestoreにアクセス
- ```where name = "リクエストされた名前"```の条件に一致するdocumentをリターンします。

新しい関数としてこのソースをデプロイした後、テストしてみます。



## コンソールとcurlでテスト
- コンソールから条件を設定してのテスト
  - name="Alice"に一致するデータが返ってきます。
![FireShot Capture 005 - Cloud Functions - sample-project - Google Cloud Platform_ - console.cloud.google.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/e8b6db64-3fec-c605-aa46-80358c109d78.png)

- curlでのテスト
 - GET/POSTのいずれでも、name="Bob"に一致するデータが返ってきます。


```:GET(クエリ文字列)
$ curl https://<リージョン>-<プロジェクトID>.cloudfunctions.net/getFilteredUser?name=Bob
{"users": [{"age": 21, "name": "Bob", "gender": "male"}]}
```

```:POST(json)
$ curl -X POST -H "Content-Type: application/json" -d '{"name": "Bob"}' https://<リージョン>-<プロジェクトID>.cloudfunctions.net/getFilteredUser 
{"users": [{"age": 21, "gender": "male", "name": "Bob"}]}
```


## テストとデプロイ方法の改善（次回）
Cloud FuctionsとFirestoreを使った簡単なAPIはできました。
とはいえ、いちいちコンソールからデプロイして動作確認するのは非常に面倒です。
なので、ローカルで気軽にテストしたり、ローカルからデプロイしたりを、Pt2以降では試してみます。

ところで、APIを公開で作成した場合、URIを知っていれば誰からでもアクセス可能なので、気になる人はデプロイした関数を消しておきましょう。

## 参考文献
以下の記事を参考にさせていただきました。
[Google Cloud FunctionsでPythonを利用してみた(Beta利用)](https://qiita.com/kai_kou/items/dca21cdfd8375a247c2f)
[Cloud FirestoreのデータをPythonで取得する](https://qiita.com/yusukeito58/items/c77feaa25fbbe37e9953)

## 余談
ここまで書いてなんですが、モバイルアプリではCloud Functions for Firebaseを使ったほうが便利とも公式で書かれています。（使いたかったのはこれが理由...）
Javascriptが得意な方はこちらを使った方が良いかもしれません。
[Google Cloud Functions と Firebase](https://cloud.google.com/functions/docs/concepts/functions-and-firebase?hl=ja)

とはいえ、pythonしか書けない人なので、引き続きGCPで実装していきます。

その他、コメントや指摘などいただけると幸いです。


 - 次回以降の記事はこちら
   - [python×Cloud Functions×FirestoreでAPIを簡単に作ってみる Pt1](https://qiita.com/ny7760/items/9cdc3407dfda99c21ae1)
   - [python×Cloud Functions×FirestoreでAPIを簡単に作ってみる Pt2 テスト編（要約）](https://qiita.com/ny7760/items/5519dd3988ceb2326d38)
   - [python×Cloud Functions×FirestoreでAPIを簡単に作ってみる Pt2 テスト編（詳細）](https://qiita.com/ny7760/items/01dacd4df434f4a5be25)
   - [python×Cloud Functions×FirestoreでAPIを簡単に作ってみる Pt3 デプロイ編](https://qiita.com/ny7760/items/7ff05b255aac2e8fa3a3)