Get to know Firestore for web

# Get to know Firestore for webの和訳

Firebaseのチュートリアルの１つである　[Get to know Firestore for web](https://firebase.google.com/learn/codelabs/firestore-web?hl=en&continue=https%3A%2F%2Fcodelabs.developers.google.com%2F#0)
を和訳（翻訳ソフトを利用）しながら、適宜修正し作成しました。誤訳がある可能性があります。

### なぜ作成したか？

読んでいるとところどころが異なる名前になっていたので、わかりづらかったので

## 目次

1. 概要
1. FireBaseプロジェクトを作りセットアップする
1. サンプルコードを取得する
1. FireBase Command Line Interfaceをインストールする
1. ローカルサーバーを実行する
1. CloudFirestoreにデータを書き込む
1. CouldFireStoreからのデータを表示する
1. Get() data
1. データの並べ替えとフィルタリング
1. インデックスをデプロイする
1. トランザクションにデータを書き込む
1. データを保護する
1. 結論 
1. +α hosting: WebAppのデプロイ

# 1. 概要

## 完成図

![FE6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/941873/cfd4b531-2ed8-aa7c-6ab0-3a18b53d01fa.png)

## 環境

* 通常は Node.js が付属しているnpm - Node v8が推奨されています。

> npmとは、「Node Package Manager」の略で、その名の通りNode.jsのパッケージを管理するためのシステムのことです。
ここでいうパッケージとは、JavaScriptのプラグインやモジュールのことです。

## 学びのポイント

* WebアプリからCloud Firestoreにデータを読み書きする
クラウドFirestoreのデータの変化をリアルタイムで聞くことができます。
* Firebase認証とセキュリティルールを使用して、クラウド
* Firestoreのデータを保護する
* 複雑なクラウドFirestoreクエリの記述



# 2. Firebaseプロジェクトを作成して設定します

## Firebaseプロジェクトを作成する

1. Firebaseコンソールからプロジェクトの作成をクリックします。
2. FriendlyEatsと言う名前にして作成します。

## 構築するアプリケーションは、ウェブ上で利用可能ないくつかのFirebaseサービスを使用します。

* ユーザーを簡単に識別するためのFirebase認証
* 構造化データをクラウドに保存し、データが更新されるとすぐに通知を受け取るCloud Firestore
* 静的アセットをホストして提供するFirebaseHosting

## 匿名認証を有効にする

匿名ログインを使用します。

1. Firebaseコンソールで、左側のナビゲーションの"構築"セクションを見つけます。
2. `Authenication`(認証)をクリックし、`Sign-in method`タブをクリックします。
3. 匿名サインインプロバイダーを有効にして、`保存`をクリックします。

## CloudFirestoreを有効にする

このアプリはCloudFirestoreを使用して、レストランの情報と評価を保存および受信します。

1. CloudFirestoreを有効にする必要があります。Firebaseコンソールの"構築"セクションで、`Firestore`をクリックします。
1. CloudFirestoreペインでデータベースの作成をクリックします。

1. この時、全て`はい`を押します。

1. Cloud Firestoreのデータへのアクセスは、セキュリティルールによって制御されます。ルールについてはこのコードラボの後半で詳しく説明しますが、開始するには、最初にデータにいくつかの基本的なルールを設定する必要があります
1. "ルール"タブ(Firebaseコンソール)の次のルールを追加し、"OK"をクリックします公開。

```
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // 
      // 警告: これらのルールは安全ではありません! 
      // これらのルールは、後ほどコードラボでより安全なルールに置き換える予定です。
      //
      allow read, write: if request.auth != null;
    }
  }
}

```

# 3. サンプリコードを取得する
コマンドラインからGitHubリポジトリのクローンを作成します。

`git clone https://github.com/firebase/friendlyeats-web`

サンプルコードは📁`friendlyeats-web`ディレクトリに複製されているはずです。今後、コマンドラインがこのディレクトリから実行されることを確認してください。

`cd friendlyeats-web`

# 4. FireBase Command Line Interfaceをインストールする

Firebaseコマンドラインインターフェース（CLI）を使用すると、ウェブアプリをローカルで提供し、ウェブアプリをFirebaseHostingにデプロイできます。

> 注：CLIをインストールするには、通常NodeJSに付属している npmをインストールする必要があります。

1. 次のnpmコマンドを実行してCLIをインストールします。

`npm -g install firebase-tools`

2. 次のコマンドを実行して、CLIが正しくインストールされていることを確認します。

`firebase --version`

3. 次のコマンドを実行して、FirebaseCLIを承認します。

アプリのローカルディレクトリとファイルからFirebaseHostingのアプリの構成を取得するようにウェブアプリテンプレートを設定しました。ただし、これを行うには、アプリをFirebaseプロジェクトに関連付ける必要があります。

`firebase login`

4. コマンドラインがアプリのローカルディレクトリにアクセスしていることを確認してください。

5. 次のコマンドを実行して、アプリをFirebaseプロジェクトに関連付けます。

`firebase use --add`

6. プロンプトが表示されたら、プロジェクトIDを選択し、Firebaseプロジェクトにエイリアスを指定します。

エイリアスは、複数の環境（本番環境、ステージングなど）がある場合に役立ちます。
今回は`default`と言うエイリアスを設定します。

7. コマンドラインの残りの指示に従います。

以下が動作例になります。

```
:friendlyeats-web $npm -g install firebase-tools
~~~~~~~省略~~~~~~~
:friendlyeats-web $firebase --version
9.2.2
:friendlyeats-web $firebase login
~~~~~~~省略~~~~~~~
:friendlyeats-web $firebase use --add
? Which project do you want to add? friendlyeats-3d64b
? What alias do you want to use for this project? (e.g. staging) default

Created alias default for friendlyeats-3d64b.
Now using alias default (friendlyeats-3d64b)
```

# 5. ローカルサーバーを実行する
私たちは実際に私たちのアプリで作業を開始する準備ができています！アプリをローカルで実行しましょう！

1. 次のFirebaseCLIコマンドを実行します。

`firebase emulators:start --only hosting`

2. コマンドラインに次の応答が表示されます。

`hosting: Local server: http://localhost:5000`

Firebase Hostingエミュレーターを使用して 、アプリをローカルで提供しています。これで、Webアプリが [http://localhost:5000] から利用できるようになります。

3. [http://localhost:5000] でアプリを開きます。

Firebaseプロジェクトに接続されているFriendlyEatsのコピーが表示されます。

アプリは自動的にFirebaseプロジェクトに接続し、匿名ユーザーとしてサイレントサインインします。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/941873/6de20ffa-5a43-0411-dea9-3c36b7b7085c.png)

# 6. CloudFirestoreにデータを書き込む

このセクションでは、アプリのUIにデータを入力できるように、CloudFirestoreにデータを書き込みます。これはFirebaseコンソールを介して手動で行うことができ ますが、基本的なCloudFirestoreの書き込みを示すためにアプリ自体で行います。

## データ・モデル

Firestoreデータは、コレクション、ドキュメント、フィールド、およびサブコレクションに分割されます。各レストランをドキュメントとして、`restaurants`というトップレベルのコレクションに保存します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/941873/bf0b7e2e-34ca-0771-647d-a8664ddfa2b4.png)
[引用文献](https://firebase.google.com/learn/codelabs/firestore-web?hl=en&continue=https%3A%2F%2Fcodelabs.developers.google.com%2F#5)

あとで、各レビュー`ratings`を各レストランで呼び出されるサブコレクションに保存します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/941873/43314362-e27c-7910-e02b-f2aee5123dc9.png)
[引用文献](https://firebase.google.com/learn/codelabs/firestore-web?hl=en&continue=https%3A%2F%2Fcodelabs.developers.google.com%2F#5)

## Firestoreにレストランを追加する

アプリの主なモデルオブジェクトはレストランです。レストランのドキュメントをrestaurantsコレクションに追加するコードを書いてみましょう。

1. ダウンロードしたファイルから、`scripts/FriendlyEats.Data.js`を開きます。
1. `FriendlyEats.prototype.addRestaurant`関数を見つけます。
1. 関数全体を次のコードに置き換えます。

```
FriendlyEats.prototype.addRestaurant = function(data) {
  var collection = firebase.firestore().collection('restaurants');
  return collection.add(data);
};
```
上のコードは、レストランのコレクションに新しいドキュメントを追加します。ドキュメントのデータは、プレーンなJavaScriptオブジェクトから取得します。最初にCloud Firestoreコレクションのレストランへの参照を取得し、データを追加します。

## レストランを追加しましょう！

1. ブラウザでFriendlyEatsアプリに戻り、更新します。
1. `Add Mock Data`をクリックします

アプリはレストランオブジェクトのランダムなセットを自動的に生成し、addRestaurant関数を呼び出します。ただし、データの取得を実装する必要があるため（コードラボの次のセクション）、実際のWebアプリにはまだデータが表示されません。

ただし、Firebaseコンソールの[Cloud Firestore]タブに移動する と、restaurantsコレクションに新しいドキュメントが表示されるはずです。

![FE4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/941873/227c9574-32c9-7064-f93d-a5a9e990d05e.png)

次のセクションでは、CloudFirestoreからデータを取得してアプリに表示する方法を学習します。
  

# 7. CouldFireStoreからのデータを表示する

このセクションでは、Cloud Firestoreからデータを取得して、アプリに表示する方法を学習します。2つの重要なステップは、クエリの作成とスナップショットリスナーの追加です。このリスナーは、クエリに一致するすべての既存のデータについて通知され、リアルタイムで更新を受け取ります。

まず、デフォルトのフィルタリングされていないレストランのリストを提供するクエリを作成しましょう。

1. `scripts/FriendlyEats.Data.js`にファイルに戻ります。
1. `FriendlyEats.prototype.getAllRestaurants`関数を見つけます。
1. 関数全体を次のコードに置き換えます。

```
FriendlyEats.prototype.getAllRestaurants = function(renderer) {
  var query = firebase.firestore()
      .collection('restaurants')
      .orderBy('avgRating', 'desc')
      .limit(50);

  this.getDocumentsInQuery(query, renderer);
};
```

上記のコードでは、`restaurants`という名前のトップレベルのコレクションから最大50のレストランを取得するクエリを作成します。これらのレストランは、平均評価（現在はすべてゼロ）の順に並べられています。このクエリを宣言した後、データの読み込みとレンダリングを担当するメソッド`getDocumentsInQuery()`にクエリを渡します。

これを行うには、スナップショットリスナーを追加します。

1. `scripts/FriendlyEats.Data.js`ファイルに戻ります。
1. `FriendlyEats.prototype.getDocumentsInQuery`関数を見つけます。
1. 関数全体を次のコードに置き換えます。

```
FriendlyEats.prototype.getDocumentsInQuery = function(query, renderer) {
  query.onSnapshot(function(snapshot) {
    if (!snapshot.size) return renderer.empty(); // Display "There are no restaurants".

    snapshot.docChanges().forEach(function(change) {
      if (change.type === 'removed') {
        renderer.remove(change.doc);
      } else {
        renderer.display(change.doc);
      }
    });
  });
};
```

上記のコード`query.onSnapshot`では、クエリの結果に変更があるたびにコールバックをトリガーします。

* 最初にコールバックがトリガーされるのは、クエリの結果セット全体、つまりCloud Firestoreの`restaurants`コレクション全体を意味します。その後、個々のドキュメントをすべて`renderer.display`関数に渡します。
* ドキュメントが削除されると、`change.type`は`remove`に等しくなります。なので、今回はUIからレストランを削除する関数を呼び出します。

両方のメソッドを実装したので、アプリをリフレッシュして、先ほどFirebaseコンソールで確認したレストランがアプリに表示されていることを確認してください。このセクションが正常に完了した場合、アプリはCloud Firestoreでデータを読み書きできるようになりました！レストランのリストが変更されると、このリスナーは自動的に更新されます。

レストランのリストが変更されると、このリスナーは自動的に更新されます。Firebaseコンソールでレストランを手動で削除したり、名前を変更してみてください。

![FE5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/941873/943a6067-1ec1-8803-9da2-bacb69f38c00.png)

# 8.Get() data

ここまでは、`onSnapshot`を使ってリアルタイムで更新情報を取得する方法を示しましたが、それは必ずしも我々が望むものではありません。一度だけデータを取得する方が理にかなっていることもあります。

ここでは、ユーザーがアプリ内の特定のレストランをクリックしたときにトリガーされるメソッドを実装します。

1. `scripts/FriendlyEats.Data.js`ファイルに戻ります。
1. 関数`FriendlyEats.prototype.getRestaurant`を見つけます。
1. 関数全体を以下のコードで置き換えます。
```
FriendlyEats.prototype.getRestaurant = function(id) {
  return firebase.firestore().collection('restaurants').doc(id).get();
};
```
この方法を実装した後、各レストランのページを表示できるようになります。リスト内のレストランをクリックすると、レストランの詳細ページが表示されます。

今のところは、コードラボで後から評価を追加する必要があるので、評価を追加することはできません。

# 9. データの並べ替えとフィルタリング

現在、私たちのアプリではレストランのリストを表示していますが、ユーザーのニーズに基づいてフィルタリングする方法はありません。このセクションでは、Cloud Firestoreの高度なクエリを使用してフィルタリングを有効にします。

ここでは、すべての点心レストランを取得するための簡単なクエリの例を示します。

```
var filteredQuery = query.where('category', '==', 'Dim Sum')
```

その名の通り、 `where()` メソッドは、設定した制限事項を満たすフィールドを持つコレクションのメンバーのみをダウンロードします。この場合、 `category` が `Dim Sum` であるレストランのみをダウンロードします。

このアプリでは、ユーザーは複数のフィルタをチェーン化して、「サンフランシスコのピザ」や「ロサンゼルスのシーフード人気順」のような特定のクエリを作成することができます。

ユーザーが選択した複数の基準に基づいてレストランをフィルタリングするクエリを構築するメソッドを作成します。

1. `scripts/FriendlyEats.Data.js`ファイルに戻ります。
1. 関数`FriendlyEats.prototype.getFilteredRestaurants`を見つけてください。
1. 関数全体を以下のコードで置き換えてください。

```
FriendlyEats.prototype.getFilteredRestaurants = function(filters, renderer) {
  var query = firebase.firestore().collection('restaurants');

  if (filters.category !== 'Any') {
    query = query.where('category', '==', filters.category);
  }

  if (filters.city !== 'Any') {
    query = query.where('city', '==', filters.city);
  }

  if (filters.price !== 'Any') {
    query = query.where('price', '==', filters.price.length);
  }

  if (filters.sort === 'Rating') {
    query = query.orderBy('avgRating', 'desc');
  } else if (filters.sort === 'Reviews') {
    query = query.orderBy('numRatings', 'desc');
  }

  this.getDocumentsInQuery(query, renderer);
};
```

上記のコードでは、複数の `where` フィルタと単一の `orderBy` 句を追加して、ユーザーの入力に基づいた複合クエリを構築しています。このクエリは、ユーザーの要件に一致するレストランのみを返すようになりました。

ブラウザでFriendlyEatsアプリを更新し、価格、都市、カテゴリでフィルタリングできることを確認してください。テスト中、ブラウザの JavaScript コンソールに以下のようなエラーが表示されます。

```
The query requires an index. You can create it here: https://console.firebase.google.com/project/.../database/firestore/indexes?create_index=...
```

これらのエラーは、Cloud Firestoreがほとんどの複合クエリにインデックスを必要とするためです。クエリにインデックスを必要とすることで、Cloud Firestoreのスケールでの高速性を維持しています。

エラーメッセージからリンクを開くと、正しいパラメータが入力されたFirebaseコンソールのインデックス作成UIが自動的に開きます。次のセクションでは、このアプリケーションに必要なインデックスを記述してデプロイします。

# 10. インデックスをデプロイする

アプリ内のすべてのパスを探索し、各インデックス作成リンクを辿りたくない場合は、Firebase CLIを使用して簡単に多くのインデックスを一度にデプロイすることができます。

1. アプリのダウンロードしたローカルディレクトリに、`firestore.indexes.json`ファイルがあります。

このファイルには、フィルタのすべての可能な組み合わせに必要なすべてのインデックスが記述されています。

> JSONとは「JavaScript Object Notation」の略で、「JavaScriptのオブジェクトの書き方を元にしたデータ定義方法」のことです。

>JavaScriptでオブジェクトを作成する際は {} や [] などの括弧を使って記述しますが、JSONはその記法を元にしています。元々はJavaScriptで使われる想定で作成されたデータ構造なので、JavaScriptと非常に相性が良いです。現在はJavaScript以外にもPythonやJava、PHPなどの幅広い言語で使われていて、JavaScriptなどのクライアント言語とPythonなどのサーバサイド言語間のデータのやり取りで使われることが多いです。

これらのインデックスを次のコマンドでデプロイします。

```
firebase deploy --only firestore:indexes
```

これで、あなたのインデックスがライブになり、エラーメッセージが消えます。
> Tip: Cloud Firestoreのindexesについてもっと知りたい場合は, [ドキュメントを見てください](https://firebase.google.com/docs/firestore/query-data/indexing).

# 11. トランザクションにデータを書き込む

ここでは、ユーザーがレストランにレビューを投稿できる機能を追加します。これまでのところ、すべての書き込みは原子的で比較的単純なものでした。エラーが発生した場合は、ユーザーに再試行を促すか、アプリが自動的に再試行します。

このアプリにはレストランの評価を追加したいユーザーが多数いるため、複数の読み取りと書き込みを調整する必要があります。まずレビュー自体を投稿し、次にレストランの評価数`(raiting count)`と平均評価`(average raiting)`を更新する必要があります。これらのうちの1つが失敗し、他の1つが失敗した場合、データベースのある部分のデータが別の部分のデータと一致しないという一貫性のない状態になってしまいます。

幸いなことに、Cloud Firestoreにはトランザクション機能があり、1つのアトミック操作で複数の読み取りと書き込みを実行できるため、データの一貫性が保たれます。

1. `scripts/FriendlyEats.Data.js`ファイルに戻ります。
1. 関数`FriendlyEats.prototype.addRating`を見つけます。
1. 関数全体を以下のコードで置き換えてください。

```
FriendlyEats.prototype.addRating = function(restaurantID, rating) {
  var collection = firebase.firestore().collection('restaurants');
  var document = collection.doc(restaurantID);
  var newRatingDocument = document.collection('ratings').doc();

  return firebase.firestore().runTransaction(function(transaction) {
    return transaction.get(document).then(function(doc) {
      var data = doc.data();

      var newAverage =
          (data.numRatings * data.avgRating + rating.rating) /
          (data.numRatings + 1);

      transaction.update(document, {
        numRatings: data.numRatings + 1,
        avgRating: newAverage
      });
      return transaction.set(newRatingDocument, rating);
    });
  });
};
```

上記のブロックでは、レストランドキュメント内の `avgRating` と `numRatings` の数値を更新するトランザクションをトリガします。同時に、新しい`ratings`を`ratings`サブコレクションに追加します。

> Note(注意): レーティングの追加は、この特定のコードラボでトランザクションを使用するための良い例です。ただし、本番アプリでは、ユーザーによる操作を避けるために、信頼できるサーバー上で平均レーティングの計算を実行する必要があります。これを行う良い方法は、クライアントから直接レーティング ドキュメントを書き、Cloud Functions を使用して新しいレストランの平均レーティングを更新することです。

> Warning: サーバー上でトランザクションが失敗すると、コールバックも繰り返し実行されます。トランザクション コールバックの中にアプリの状態を変更するロジックを絶対に入れないでください。

# 12. データの保護
このコードラボの最初の段階では、アプリのセキュリティルールを設定して、データベースを完全にオープンにして、あらゆる読み書きができるようにしました。実際のアプリケーションでは、望ましくないデータアクセスや変更を防ぐために、もっと細かいルールを設定したいと思います。

1. Firebaseコンソールの`構築`セクションで、`Cloud Firestore`をクリックします。
1. `Cloud Firestore`セクションの`ルール`タブをクリックします。
1. デフォルトのルールを以下のルールに置き換え、`発行` をクリックします。

```
rules_version = '2';
service cloud.firestore {

  //  リクエストの前後でフィールド「key」の値が同じかどうかを判断します。
  function unchanged(key) {
    return (key in resource.data) 
      && (key in request.resource.data) 
      && (resource.data[key] == request.resource.data[key]);
  }

  match /databases/{database}/documents {
    // Restaurants:
    //   - Authenticated user can read
    //   - Authenticated user can create/update (for demo purposes only)
    //   - Updates are allowed if no fields are added and name is unchanged
    //   - Deletes are not allowed (default)
    // 飲食店です。
    // - 認証されたユーザーは読むことができます。
    // - 認証されたユーザーが作成/更新することができます(デモ目的のみ)
    // - フィールドが追加されておらず、名前が変更されていない場合、更新が許可されます。
    // - 削除は許可されません(デフォルト)
    match /restaurants/{restaurantId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth != null
                    && (request.resource.data.keys() == resource.data.keys()) 
                    && unchanged("name");
      
      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
      //   - Deletes and updates are not allowed (default)
      // 評価しています。
      // - 認証されたユーザーが読むことができます
      // - 認証されたユーザは、userIdが一致していれば作成可能です。
      // - 削除と更新は許可されません(デフォルト)
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
      }
    }
  }
}
```

これらのルールは、クライアントが安全な変更を行うだけであることを保証するためにアクセスを制限します。例えば、以下のようになります。

* レストランのドキュメントを更新しても、名前やその他の不変のデータではなく、評価のみを変更することができます。
* 格付けは、ユーザーIDがサインインしたユーザーと一致している場合にのみ作成できるので、なりすましを防ぐことができます。

Firebaseコンソールを使用する代わりに、Firebase CLIを使用してFirebaseプロジェクトにルールを展開することもできます。作業ディレクトリのfirestore.rulesファイルには、上記のルールがすでに含まれています。これらのルールをローカルのファイルシステムから(Firebaseコンソールを使うのではなく)デプロイするには、以下のコマンドを実行します。

```
firebase deploy --only firestore:rules
```

> Important: To learn more about security rules, have a look at the [security rules documentation](https://firebase.google.com/docs/firestore/security/get-started).

# 13.  結論
このコードラボでは、Cloud Firestore を使って基本的な読み書きを実行する方法と、セキュリティルールを使ってデータアクセスを安全にする方法を学びました。完全なソリューションは、[Quickstarts-js repository](https://github.com/firebase/quickstart-js/tree/master/firestore)にあります。

Cloud Firestore の詳細については、以下のリソースを参照してください。

* [Cloud Firestoreの紹介](https://firebase.google.com/docs/firestore/)
* [データ構造の選択](https://firebase.google.com/docs/firestore/manage-data/structure-data)
* [クラウドファイヤーストアのWebサンプル](https://firebase.google.com/docs/firestore/client/samples-web)

# 14. +α hosting: WebAppのデプロイ

1. まず、ローカルサーバーを停止させましょう。

`control + C`で終了させます。

2. `Firebaseコンソール`から`構築`タブの中にある`hosting`タブをクリックします。

3. 手順に沿って実行していきましょう。ここまでの手順でCLIとfirebaseの初期化もsているので
`firebase deploy`をコマンド入力します。

ターミナルに表示されたwebサイトを見ると公開されていることがわかります！