Cloud FunctionsからBigQueryへ自分で作成したテーブルにデータを追加する

# 3行で

- Firebase Extensionsや、Cloud SchedulerでバックアップしたFirestoreのデータではなく、特定のリクエストをトリガーにBigQueryへデータを追加したかった
- 自分で作成したデータセット、テーブル、スキーマにデータを追加できるので分析がしやすくなりました
- BigQueryのドキュメントがなかなか見つからず辛かったので記事にしました

# 実装のきっかけ

今年の9月に月額課金をもつサービスをリリースしました[^1]。
このサービスのWebの解約画面に「解約理由」を必須にして欲しいと要望がありました。

「解約理由」は次の要件でした。

- チェックボックス形式で解約理由の項目を選択できる（必須かつ複数選択可）
- テキスト形式で自由に解約の理由について書くことが出来る（任意）
- それぞれの解約理由の合計を知りたい

実際に開発した解約理由の画面 | 
:-: |
<img width="1483" alt="image1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/be4c0c47-c3ff-ac01-3242-b011a4bcec4f.png"> | 


# Cloud FunctionsからBigQueryへデータを送信する手順

## 1. Cloud Functionsのコード

BigQueryを準備する前に、先にコードを見せた方が理解しやすいと思うのでコードを載せました。
解約処理は、本記事には関係ないので「解約理由をBigQueryに送信する」コードを中心にして書き換えてます。

余談ですが、 BigQueryのドキュメントを探すのが一番苦労したのでコードのコメントにも追記しています。
探しにくいと感じるのは僕だけでしょうか 😭

- https://cloud.google.com/nodejs/docs/reference/bigquery/3.0.x/Table#insert

```js:cancelPayment.ts
import * as functions from 'firebase-functions';
import { BigQuery } from '@google-cloud/bigquery';

interface CancelReason {
  id: number;
  title: string;
}

/*
 *  月額プランを解約する
 */
export = functions
  .region('asia-northeast1')
  .https.onCall(async (data, context) => {
    if (!context.auth) {
      throw new functions.https.HttpsError(
        'unauthenticated',
        '認証エラー',
        data
      );
    }

    if (!data.reasons || data.reasons.length === 0) {
      throw new functions.https.HttpsError(
        'invalid-argument',
        '解約の理由を選択してください',
        data
      );
    }

    try {
      // ここで解約（処理は省略）
      await sendBigQuery(context.auth.uid, data);
    } catch (error) {
      throw new functions.https.HttpsError(error.code, error.message, data);
    }
  });

/*
 * 解約理由をBigQueryに送信する
 * doc: https://cloud.google.com/nodejs/docs/reference/bigquery/3.0.x/Table#insert
 */
async function sendBigQuery(
  uid: string,
  data: { reasons: CancelReason[]; comment: string }
) {
  try {
    const bigQuery = new BigQuery({ projectId: process.env.GCLOUD_PROJECT });
    const table = bigQuery.dataset('データセットID').table('cancel_payment');
    const isExists = await table.exists();

    if (!isExists[0]) {
      console.error(`🧨 sendBigQuery: cancel_paymentテーブルがない`);
      return;
    }

    const ids: number[] = data.reasons.map(v => v.id);
    await table.insert({
      TIMESTAMP: bigQuery.timestamp(new Date()),
      UID: uid,
      DATA: JSON.stringify(data),
      COMMENT: data.comment,
      REASON_1: ids.includes(1) ? 1 : 0,
      REASON_2: ids.includes(2) ? 1 : 0,
      REASON_3: ids.includes(3) ? 1 : 0,
      REASON_4: ids.includes(4) ? 1 : 0,
      REASON_5: ids.includes(5) ? 1 : 0,
      REASON_6: ids.includes(6) ? 1 : 0,
    });
  } catch (e) {
    console.error(`🧨 sendBigQuery: ${JSON.stringify(e)}`);
    throw new functions.https.HttpsError(
      'internal',
      '解約処理中にエラーが発生しました'
    );
  }
}
```

## 2. BigQueryでデータセット、テーブルを作成する

```js
const bigQuery = new BigQuery({ projectId: process.env.GCLOUD_PROJECT });
const table = bigQuery.dataset('データセットID').table('cancel_payment');
```

上記のとおり、連携したFirebaseのプロジェクトのBigQueryにデータセットとテーブルを作成します。
要件どおりにスキーマの型や説明を加えて分析しやすくします。

BigQueryにデータセットとテーブルを作成 | スキーマ |
:-: | :-: |
<img width="1270" alt="imag2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/3cfaa644-0327-2f58-72f6-0e09021a6f0b.png"> | <img width="1270" alt="image3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/4051a3e3-d1e3-471b-f22e-6f3503e36811.png"> |

データセット、テーブル、スキーマの各ドキュメントは下記のページにあります。

- https://cloud.google.com/bigquery/docs/datasets-intro?hl=ja
- https://cloud.google.com/bigquery/docs/tables-intro?hl=ja
- https://cloud.google.com/bigquery/docs/schemas?hl=ja


## 3. クエリで分析する

1.のコードをデプロイして解約理由がBigQueryに送信されてるかクエリを実行して確かめます。

```sql:解約理由の集計クエリ
SELECT
  count(uid) AS cancel_count,
  COUNT(CASE
      WHEN reason_1=1 THEN reason_1
    ELSE
    NULL
  END
    ) AS reason_1,
  COUNT(CASE
      WHEN reason_2=1 THEN reason_2
    ELSE
    NULL
  END
    ) AS reason_2,
  COUNT(CASE
      WHEN reason_3=1 THEN reason_3
    ELSE
    NULL
  END
    ) AS reason_3,
  COUNT(CASE
      WHEN reason_4=1 THEN reason_4
    ELSE
    NULL
  END
    ) AS reason_4,
  COUNT(CASE
      WHEN reason_5=1 THEN reason_5
    ELSE
    NULL
  END
    ) AS reason_5,
  COUNT(CASE
      WHEN reason_6=1 THEN reason_6
    ELSE
    NULL
  END
    ) AS reason_6
FROM
  `プロジェクト名.データセット名.cancel_payment`
```

クエリの結果 | 
:-: | 
<img width="681" alt="imag4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37385/eece4fcd-e0b0-03ac-c376-f0f86fb4deaf.png"> | 


# 実装時の注意点

BigQueryのデータセットやテーブルを作成し直したり、スキーマを変更しても、即座に変更が反映されるわけではないようです。

最大でも1分ぐらい待ってから、Cloud Functionsの関数にリクエストを送った方が期待した結果が返ってきます。

BigQuery側のデータを変更したとき、たまに正常に動かない時があったので知っておくと良いかもしれません。

# おわりに

やってることはドキュメントを見れば理解できるので簡単な実装です。
しかし、それ故に自分と同じことを考える記事を見かけませんでした。
僕の検索力が低いだけかもしれないですが。

ただ、解約理由の要件が複数選択ではなく一つだけ選択の場合、Google Analyticsにデータを送信する方が圧倒的に楽です。
もし、このような複数選択のデータでも、Google Analyticsで簡単に分析できる方法を知っている人がいれば、記事にしてほしいです 🥺

コードを書く前に、最小工数で要件を満たせるよう、FirebaseやBigQueryを使い倒していきたいですね 😄

[^1]: [FlutterとFirebaseで新しいサービスをリリースしました](https://tech.studyplus.co.jp/entry/2019/09/27/113643)