FirebaseのRealtime Databaseで簡易投票システムを作ってみる 

## 概要
社内の懇親イベントで、Web上での投票ページが必要になりました。
しかし1回きりのイベントなので、そんなに工数をかけたくない。
そうだ、Firebaseを使ってみよう！

## Firebaseで何ができる？
Firebase統合バックエンドサービスであり、
本来自力でサーバーサイドで構築するようなDBや認証機能などを提供しています。
またWeb,iOS,Androidなど多くのプラットフォームに対応しています。

従量課金のサービスではありますが、小規模な開発には充分な無料枠が設定されています。
（Sparkプランの場合。大規模向けのBlazeプランは完全従量課金制）

料金の詳細は[こちら](https://firebase.google.com/pricing)

コンソール画面から各種サービスの利用を開始することができます。

### Firebaseの主な機能

**アナリティクス（Analytics）**
Googleが提供するGCPのサービスであるため、Googleアナリティクスが標準で利用できます。
Firebaseコンソールの管理画面でアクセス推移の確認が容易にできます。

**データベース（Realtime Database）**
NoSQL形式のデータベース。
変更が即座に接続されているデバイスに反映されるリアルタイムの同期機能をもちます。

**認証（Authentication）**
メール・パスワードでのログイン機能のほか、
TwitterやFacebookなどのSNS認証や、電話番号によるSMS認証(有料)も組み込めます。

**ストレージ（Cloud Storage）**
クラウドストレージが5GBまで無料で利用できます。

**サーバーレスコンピューティング（Cloud Functions）**
ファンクション単位で実行できるサーバーレス機能。
データベースの更新をトリガーにして起動させるといったことが可能です。

## 作りたいもの
* 立候補者が複数名いる、賞争いの投票システム
* 不特定多数の参加者が、候補者うちいずれかに投票する
* 参加者1人につき、1回のみ投票できる

今回は投票結果を保持するためにRealtime Databaseを、
投票のタイミングで集計を行うためにCloud Functionsを利用しました。
また、投票回数はクッキーを用いて制限しましたが、Firebaseの機能とは関係がないため割愛します。

## Firebaseアプリ開発を始める
<img width="727" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/d45ce7be-fa2a-964b-3f34-0b53bbb82c9e.png">
コンソールはこんな感じです。
左側のナビゲーションエリアに、Firebaseで利用できる各機能が表示されています。

<img width="727" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/d9142047-7a39-13de-ecbb-d11aba2af1f4.png">
「プロジェクトを追加」から新規作成します。

<img width="727" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/c5807bca-1802-9dea-1ddb-6f041443678b.png">
プロジェクト名を決めます。
これはFirebaseサービス全体でユニークなものである必要があります。

<img width="727" alt="スクリーンショット 2020-02-02 20.48.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/7d8d5121-76b7-1711-2320-a7cd0dcbc933.png">
アナリティクスを利用したい場合は、有効にしましょう。
その場合、登録済みのGoogleアナリティクスアカウントと連携させます。

<img width="1010" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/355b063c-7eb8-2f56-d376-ed2e63fc90ec.png">
無事プロジェクトが作られました！
今回はWebサイトにFirebaseを導入するので、</>ボタンを押します。

<img width="926" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/1860838c-883a-dba8-d532-e71428b7be98.png">
アプリ名は「event」「イベント用」など、自分が識別しやすいものにできます。
プロジェクト名と違い、Firebase内でユニークである必要はありません。

<img width="947" alt="スクリーンショット 2020-02-02 21.04.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/26e133a1-3e77-df4a-f2ae-e4ad130a9d0f.png">
スクリプトをコピーし、Webサイトに貼り付けます。これは一番簡単なSDK（Software Development Kit）を読み込んで利用する方法です。
上記に加え、利用したい機能ごとにスクリプトを追加で読み込ませる場合があります。
各SDKについて詳しくは[こちら](https://firebase.google.com/docs/web/setup?hl=ja)を参照してください。

```html:index.html
<!-- すべてのFirebaseアプリで必要 -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-app.js"></script>
<!-- Realtime Databaseを利用するには下のSDKを追加で読み込ませます -->
<script src="https://www.gstatic.com/firebasejs/7.5.0/firebase-database.js"></script>

<!-- 投票のスクリプト -->
<script src="/path_to/vote.js">
```

#### 注： セキュリティルールについて
<img width="513" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/6339dffe-372e-d392-a05c-dc3e6f3d43aa.png">
Firebaseでは、データベースへのアクセスルールを設定できます。
セキュリティルールの`.read`および`.write`を`true`に設定すると、任意のスクリプトからの接続が可能となります（外部からの改ざんが可能である旨の警告が出る）。
今回は会員登録も存在しない簡易な投票システムを作るため、不特定多数のアクセスを許可するテストモードで作成しました。
しかし、データの適切な保護をするために、認証機能と合わせるなど[セキュリティルールの設定](https://firebase.google.com/docs/database/security/quickstart?authuser=0)が必要不可欠であるとドキュメントで言及されています。


## 実際に投票システムを作る

### アプリの初期化

```javascript:vote.js
//Firebaseの接続設定
const firebaseConfig = {
  apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  authDomain: "PROJECTNAME.firebaseapp.com",
  databaseURL: "https://PROJECTNAME.firebaseio.com",
  projectId: "PROJECTNAME",
  storageBucket: "PROJECTNAME.appspot.com",
  messagingSenderId: "00000000000",
  appId: "1:XXXXXXXXXXXX:web:XXXXXXXXXXXXXXXXXXXX",
  measurementId: "XXXXXXXXXXX"
};

//初期化
firebase.initializeApp(firebaseConfig);
```


### 投票処理

`firebase.database()`でRealtime Databaseにアクセスできます。
RealtimeデータベースはNoSQLであり、JSON形式でデータを保持します。
パスを指定してその階層に読み書きを行うようなイメージです。

```javascript
{
  votes: {
    LwY0fIos2fdKi46zvuw: {
      for: 'Andy',
      created: '2020/02/02 02:02:02',
    },
    LwY0px7Rx7y4qISIiJq: {
      for: 'Bob',
      created: '2020/02/02 02:03:04',
    },
  }
}
```

今回は一票ごとにユニークなキーを持たせ、
そのメンバーとして投票に関する候補者や時刻などの情報を持たせました。

```javascript:vote.js
...

//投票する
function vote (candidate) {
　//票ごとにユニークなキーを取得する
  let key = firebase.database().ref('votes').push().key;

  //投票データ
  let values = {
    'for': candidate,
    'created': new Date().toLocaleString(),
  };

  //作成したキーに投票データを代入する
  let updates = {};
  updates['/votes/' + key] = values;

  //データ更新
  firebase.database().ref().update(updates);
}
```

ここで送信したデータは、コンソール上で確認することができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/39fc37fa-4abb-16b1-e6d6-68b9d41aa6fe.png">

### 集計処理
投票データはこれでFirebase上に保存できましたが、
結果表示用に、このデータを集計したものが欲しいです。

Cloud Functionの機能を用いると、
Realtime Databaseに更新をトリガーにして処理を実行させることができます。

```
npm install -g firebase-tools
```

Firebase CLI用いてローカルで開発し、Firebase上にデプロイします。
https://firebase.google.com/docs/functions/get-started?hl=ja


```javascript:index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

//votesに書き込みがあったタイミングで実行される
exports.countVotes = functions.database.ref('/votes').onWrite(change => {
  // onWriteに渡されるオブジェクトは、変更前beforeと変更後afterにわかれている
  let changed = change.after.val();
  let countAll = Object.keys(changed).length;

  // 候補者
  const candidates = ['Andy', 'Bob'];

  // 得票数countの初期化
  let count = {
    'all': countAll,
  }
  for (let cndkey in candidates) {
    count[candidates[cndkey]] = 0;
  }
  //{'all': 0, 'andy': 0, 'bob': 0}

  // 集計
  for (let key in changed) {
    for (let cndkey in candidates) {
      if (changed[key]['for'] === candidates[cndkey]) {
        count[candidates[cndkey]] += 1;
      }
    }
  }
  //votesと同じ階層に、集計結果countを保存
  return change.after.ref.parent.child('count').set(count);
});

```
データベースのvotesに変更があったタイミング(onWriteイベント)で、すべての投票を集計し、
countに保存するトリガー関数を作成します。

作り終わったら

```bash
firebase deploy --only functions
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/f38f7e2f-0564-7b06-48ac-fbac88de08c7.png">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/cdf5a8f3-845a-8ab7-a087-a8e26a821ad2.png">

デプロイした関数はコンソール上で確認することができます。

<img width="729" alt="スクリーンショット 2020-02-09 17.48.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/163621/f3765794-5e0b-1299-e97d-2fdadc60129c.png">


votesに投票データが挿入されるたびに、countが更新されます。

### 結果取得処理

Realtime Databaseを用いると、
データに変更があったタイミングでイベント(valueイベント)を発生させることができます。
今回の場合は投票がされたタイミングでリアルタイムに結果に反映されることになります。

```javascript:result.js
function getCount () {
  let counts = firebase.database().ref('/count');
  //countが更新されるたびに実行
  counts.on('value', snapshot => {
    console.log(snapshot.val());
　　//データ表示処理
  });
}
```



## まとめ
結論から言うと、Firebase自体の学習コストは多少あるので、
普通のRDBS使った方が場合によっては早く実装できるかもしれません。