Cloud FunctionsをPythonで書く

# この記事について
Cloud FunctionでPythonを使用するためのサンプルです
[こちら](https://github.com/hideto1198/GCP_Python "GitHub")からサンプルコードのダウンロードができます。

# なぜPython?
好きだから。これに限ります。Pythonは遅いとよく言われますが、使用するメモリ量などを調整することでマシになりますし、
自分のプロジェクトでは影響が出るほどでもないからです。限界まで速さを求めるなら他の言語が良いでしょう

# はじめに
Cloud Functionsのデプロイはかなり遅いです。プログラムミスで再デプロイする時の時間ほど苦痛な時はありません。
なので僕は基本的にVSCode上でテストプログラムを作成してからCloud Functionsにデプロイしています。
コードチェックもありませんので、間違えててもそのまま通過します。（デプロイは失敗します）
ローカルでテストしてデプロイするのがベターでしょう
ローカルではサービスアカウントのファイルを使用します。
`Firebase -> 任意のプロジェクト -> プロジェクトの設定 -> サービスアカウント`
よりjsonファイルをダウンロードしてください。


# requirements
## ローカル
googleライブラリがインストールされていない場合はインストールしてください。
```cmd
pip install google
```

> firebase_adminを使用するパターンも見ますがこちらはCloud Functions上での展開に時間がかかりますので、
> ローカルでもgoogleを使用します。
## Cloud functions上
```txt: requirements.txt
# Function dependencies, for example:
# package>=version
google-cloud-firestore==1.2.0
```
Cloud functions上にデプロイするときはrequirements.txtに`google-cloud-firestore`を追加しなければなりません。
これがないとエラーが出てデプロイできません。

# おまじない
```python
import json #　後ほど説明します
from google.cloud import firestore
from google.oauth2.service_account import Credentials

# 以下ローカルで使用する場合
cred = Credentials.from_service_account_file(filename='YOUR_SERVICE_ACCOUNT_FILE_PATH')
db = firestore.Client(credentials=cred)

# Cloud Functionsにデプロイする場合
db = firestore.Client()

# response用ヘッダー
headers = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET,POST',
    'Access-Control-Allow-Headers': 'Content-Type',
    'Access-Control-Max-Age': '3600'
}
```

> Credentialsはローカルでのみ使用しますので、デプロイ時は削除してください

# CORS対応
Webアプリケーションから呼び出す際に、実行前にGoogle Chrome（他もあると思われますが）から`preflight request`というものが送信されます。
このrequestを正しく処理しないとエラーになり、結果を取得することができません。

```python
def main(request):
    # preflight requestのmethodはOPTIONだそう。
    if request.method == 'OPTIONS':
        headers = {
            'Access-Control-Allow-Origin': '*', # ここは自分のドメインに変える
            'Access-Control-Allow-Methods': 'GET', # or 'POST', 'GET,POST' 許可するmethodのタイプを記入 
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Max-Age': '3600'
        }

        return ('', 204, headers)
    """
    ここにメイン処理をかく
    """
    return (jsons, 200, headers)
```

# コレクションからデータを取得する
```python
# エントリポイントをmainにする(自由)
def main(request):

    # ローカル用
    data = request['data']
    
    """ デプロイ用
    request_json = request.get_json()
    data = request_json['data']
    """

    snapshots = db.collection('USERS').get()
    # or
    snapshots = db.collection('USERS').stream()

    for snapshot in snapshots:
        print(snapshot.id) # ドキュメントID
        print(snapshot.to_dict()) # ドキュメント内のフィールド
    
    jsons = json.dumps({'key': 'value'}, ensure_ascii=False)
    return (jsons, 200, headers)

# ローカル用　デプロイ時は削除する
if __name__ == '__main__':
    request = {
    }
    main(request)
```

# ドキュメントからデータを取得する
```python
# エントリポイントをmainにする(自由)
def main(request):

    # ローカル用
    data = request['data']
    
    """ デプロイ用
    request_json = request.get_json()
    data = request_json['data']
    """

    datas = db.collection('USERS').document(data['userID']).get().to_dict()

    for key, value in datas.items():
        print(key, value)

    jsons = json.dumps({'key': 'value'}, ensure_ascii=False)
    return (jsons, 200, headers)

# ローカル用　デプロイ時は削除する
if __name__ == '__main__':
    request = {
        'data': {
            'userID': '12345'
        }
    }
    main(request)
```

> `to_dict()`メソッドで辞書型としてデータを扱うことができる

# getメソッドとstreamメソッドの違い
Firebaseのドキュメントでは`collection`からのデータ取得の場合は`stream()`を使用し、
`document`からの取得では`get()`を使用していました。
しかし、`collection`からデータを取得する際にも`get()`が使用できますので、違いがわかりません。
逆に`document`には`stream()`が使用できません。
強いていうならば、`stream()`は一度しか参照できません。以下のコードを実行するとわかります。

```python
def main(request):

    # ローカル用
    data = request['data']
  
    snapshots = db.collection('USERS').get()
    # snapshots = db.collection('USERS').stream()
    print(snapshots)

    i = 1
    while int(i) != 0:
        snapshots
        for snapshot in snapshots:
            print(snapshot.id)
            print(snapshot.to_dict())
        i = input()

if __name__ == '__main__':
    request = {
        'data': {
            'userID': '12345'
        }
    }
    main(request)
```
`get()`の時は何度も結果が表示されるのに対して、`stream()`は一度しか表示されないことがわかります。


# フィールド、ドキュメント、コレクションの削除
```python
# エントリポイントをmainにする(自由)
def main(request):

    # ローカル用
    data = request['data']
    
    """ デプロイ用
    request_json = request.get_json()
    data = request_json['data']
    """

    user_ref = db.collection('USERS').document(data['userID'])

    # フィールドを削除する場合
    user_ref.set({'favorite': firestore.DELETE_FIELD}, merge=True)
    # or
    user_ref.update({'favorite': firestore.DELETE_FIELD})

    # ドキュメントを削除する場合
    user_ref.collection('datas').document('books').delete()
    
    # コレクションを削除する
    # 仕様上、コレクションを削除するにはコレクションの配下にある全てのデータを取得する必要がある。
    datas = user_ref.collection('datas').get()
    for data in datas:
        data.reference.delete()
    
    jsons = json.dumps({'key': 'value'}, ensure_ascii=False)
    return (jsons, 200, headers)

# ローカル用　デプロイ時は削除する
if __name__ == '__main__':
    request = {
        'data': {
            'userID': '12345'
        }
    }
    main(request)
```

# レスポンスの文字化け対応
クライアントでデータ受信後、日本語が文字化けしてしまう時があります。
その時は`json.dumps()`を使用します。
```python
# レスポンスの部分のみです
jsons = json.dumps({'key': 'ばりゅ〜'}, ensure_ascii=False)
return (jsons, 200, headers)
```

`ensure_ascii=False`を使うことで日本語の文字化けを防ぐことが可能です。
# 終わり
今回は以上で終わりにします。
また、続きが出来次第投稿したいと思います。