Cloud Firestore ウェブ Codelab

1. 概要

目標

この Codelab では、Cloud Firestore を利用したレストラン レコメンデーション ウェブアプリを作成します。

img5.png

ラボの内容

  • ウェブアプリから Cloud Firestore へのデータの読み取りと書き込みを行う
  • Cloud Firestore データの変更をリアルタイムでリッスンする
  • Firebase Authentication とセキュリティ ルールを使用して Cloud Firestore データを保護する
  • 複雑な Cloud Firestore クエリを作成する

必要なもの

この Codelab を始める前に、以下がインストールされていることを確認してください。

  • npm は、通常は Node.js に付属しています。Node 16 以降が推奨されます。
  • 任意の IDE またはテキスト エディタ(WebStormVS CodeSublime など)

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

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

  1. Firebase コンソールで [プロジェクトを追加] をクリックし、Firebase プロジェクトに「FriendlyEats」という名前を付けます。

Firebase プロジェクトのプロジェクト ID を覚えておいてください。

  1. [プロジェクトの作成] をクリックします。

これから構築するアプリケーションは、ウェブで入手できるいくつかの Firebase サービスを使用します。

  • ユーザーを簡単に識別できる Firebase Authentication
  • Cloud Firestore: 構造化データをクラウドに保存し、データが更新されるとすぐに通知を受け取ります。
  • Firebase Hosting: 静的アセットをホストして提供します。

この Codelab では、すでに Firebase Hosting を構成しています。ただし、Firebase Auth と Cloud Firestore については、Firebase コンソールを使用してサービスを構成し、有効にする手順を説明します。

匿名認証を有効にする

この Codelab では認証に焦点を当てませんが、アプリでなんらかの認証方法を用意することが重要です。ここでは匿名ログインを使用します。つまり、ユーザーはプロンプトを表示せずにサイレント ログインを行います。

匿名ログインを有効にする必要があります。

  1. Firebase コンソールの左側のナビゲーションで [ビルド] セクションを見つけます。
  2. [Authentication] をクリックし、[Sign-in method] タブをクリックします(またはこちらをクリックして、直接移動します)。
  3. [匿名 ログイン プロバイダ] を有効にして [保存] をクリックします。

img7.png

これにより、ユーザーがウェブアプリにアクセスするときに、暗黙のログインが可能になります。詳しくは、匿名認証に関するドキュメントをご覧ください。

Cloud Firestore の有効化

このアプリは Cloud Firestore を使用して、レストランの情報や評価の保存と受信を行います。

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

Cloud Firestore のデータへのアクセスは、セキュリティ ルールによって制御されます。ルールについては、この Codelab で後ほど詳しく説明しますが、まずはデータに対して基本的なルールをいくつか設定する必要があります。Firebase コンソールの [ルール] タブで以下のルールを追加し、[公開] をクリックします。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      //
      // WARNING: These rules are insecure! We will replace them with
      // more secure rules later in the codelab
      //
      allow read, write: if request.auth != null;
    }
  }
}

上記のルールにより、データアクセスをログインしているユーザーのみに制限できます。これにより、認証されていないユーザーによる読み取りと書き込みが防止されます。これは、公開アクセスを許可するよりも優れていますが、それでも安全とはほど遠いため、これらのルールはこの Codelab の後半で改善される予定です。

3. サンプルコードを取得する

コマンドラインから GitHub リポジトリのクローンを作成します。

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

サンプルコードのクローンは、ꛭfriendlyeats-web ディレクトリに作成しておく必要があります。これからは、このディレクトリからすべてのコマンドを実行してください。

cd friendlyeats-web/vanilla-js

スターター アプリをインポートする

IDE(WebStorm、Atom、Sublime、Visual Studio Code など)を使用して、⏎friendlyeats-web ディレクトリを開くか、インポートします。このディレクトリには、まだ機能していないレストランのおすすめアプリで構成される Codelab の開始コードが含まれています。この Codelab 全体を通じて機能するので、このディレクトリのコードを編集する必要があります。

4. Firebase コマンドライン インターフェースをインストールする

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

  1. 次の npm コマンドを実行して CLI をインストールします。
npm -g install firebase-tools
  1. 次のコマンドを実行して、CLI が正しくインストールされていることを確認します。
firebase --version

Firebase CLI のバージョンが v7.4.0 以降であることを確認します。

  1. 次のコマンドを実行して、Firebase CLI を承認します。
firebase login

アプリのローカル ディレクトリとファイルから Firebase Hosting の構成を pull するためのウェブアプリ テンプレートを設定しました。そのためには、アプリを Firebase プロジェクトに関連付ける必要があります。

  1. コマンドラインでアプリのローカル ディレクトリにアクセスしていることを確認します。
  2. 次のコマンドを実行して、アプリを Firebase プロジェクトに関連付けます。
firebase use --add
  1. プロンプトが表示されたら、プロジェクト ID を選択し、Firebase プロジェクトにエイリアスを指定します。

エイリアスは複数の環境(本番環境、ステージングなど)がある場合に役立ちます。ただし、この Codelab では default のエイリアスのみを使用します。

  1. コマンドラインの残りの手順に沿って操作します。

5. ローカル サーバーを実行する

実際にアプリの作業を開始する準備が整いました。アプリをローカルで実行してみましょう。

  1. 次の Firebase CLI コマンドを実行します。
firebase emulators:start --only hosting
  1. コマンドラインに次のレスポンスが表示されます。
hosting: Local server: http://localhost:5000

ここでは、アプリをローカルで提供するために Firebase Hosting エミュレータを使用します。これで、ウェブアプリが http://localhost:5000 から使用できるようになります。

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

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

アプリは Firebase プロジェクトに自動的に接続され、匿名ユーザーとしてサイレント ログインされます。

img2.png

6. Cloud Firestore にデータを書き込む

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

データモデル

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

img3.png

その後、各レビューを各レストランの ratings というサブコレクションに保存します。

img4.png

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

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

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

FriendlyEats.Data.js

FriendlyEats.prototype.addRestaurant = function(data) {
  var collection = firebase.firestore().collection('restaurants');
  return collection.add(data);
};

上記のコードでは、restaurants コレクションに新しいドキュメントを追加します。ドキュメント データはプレーンな JavaScript オブジェクトから取得されます。これを行うには、まず Cloud Firestore コレクション restaurants への参照を取得してから、データを add します。

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

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

アプリは、レストラン オブジェクトのランダムなセットを自動的に生成し、addRestaurant 関数を呼び出します。ただし、実際のウェブアプリではまだデータを確認できません。これは、データの取得を実装する必要があるためです(Codelab の次のセクション)。

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

img6.png

これで、ウェブアプリから Cloud Firestore にデータが書き込まれました。

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

7. Cloud Firestore のデータを表示する

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

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

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

FriendlyEats.Data.js

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 ファイルに戻ります。
  2. FriendlyEats.prototype.getDocumentsInQuery 関数を見つけます。
  3. 関数全体を次のコードに置き換えます。

FriendlyEats.Data.js

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.typeremoved と同じになります。ここでは、UI からレストランを削除する関数を呼び出します。

両方のメソッドを実装したので、アプリを更新して、先ほど Firebase コンソールで確認したレストランがアプリに表示されることを確認します。このセクションを正常に完了すると、アプリは Cloud Firestore でデータの読み取りと書き込みを行います。

レストランのリストが変更されると、このリスナーが自動更新し続けます。Firebase コンソールに移動して、手動でレストランを削除するか、名前を変更してみてください。変更内容がすぐにサイトに反映されます。

img5.png

8. Get() データ

ここまでは、onSnapshot を使用してリアルタイムで更新を取得する方法を紹介してきましたが、必ずしもそれが必要なわけではありません。場合によっては、データを 1 回だけ取得することをおすすめします。

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

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

FriendlyEats.Data.js

FriendlyEats.prototype.getRestaurant = function(id) {
  return firebase.firestore().collection('restaurants').doc(id).get();
};

このメソッドを実装すると、各レストランのページを表示できるようになります。リスト内のレストランをクリックすると、そのレストランの詳細ページが表示されます。

img1.png

この Codelab の後半で評価の追加を実装する必要があるため、現時点では評価を追加できません。

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

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

すべての Dim Sum 件のレストランを取得する簡単なクエリの例を次に示します。

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

where() メソッドは、その名前が示すように、設定した制限を満たすフィールドを持つコレクションのメンバーのみをクエリでダウンロードします。この例では、categoryDim Sum のレストランのみがダウンロードされます。

このアプリでは、ユーザーは「サンフランシスコのピザ」や「人気順でロサンゼルスのシーフードを注文」など、複数のフィルタを連結して特定のクエリを作成できます。

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

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

FriendlyEats.Data.js

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/project-id/database/firestore/indexes?create_composite=...

これらのエラーは、Cloud Firestore がほとんどの複合クエリに対してインデックスを必要とするためです。クエリでインデックスを必須にすることで、Cloud Firestore は大規模な処理でも高速になります。

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

10. インデックスのデプロイ

アプリのすべてのパスを探索して各インデックス作成リンクをたどる必要がなければ、Firebase CLI を使用して一度に多数のインデックスを簡単にデプロイできます。

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

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

firestore.indexes.json

{
 "indexes": [
   {
     "collectionGroup": "restaurants",
     "queryScope": "COLLECTION",
     "fields": [
       { "fieldPath": "city", "order": "ASCENDING" },
       { "fieldPath": "avgRating", "order": "DESCENDING" }
     ]
   },

   ...

 ]
}
  1. 次のコマンドを使用して、これらのインデックスをデプロイします。
firebase deploy --only firestore:indexes

数分後、インデックスが有効になり、エラー メッセージが表示されなくなります。

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

このセクションでは、ユーザーがレストランにレビューを送信できる機能を追加します。これまでのところ、すべての書き込みはアトミックで比較的シンプルでした。いずれかのエラーが発生した場合は、ユーザーに再試行を求めるか、アプリが自動的に書き込みを再試行します。

アプリにはレストランの評価を追加するユーザーが多数存在するため、複数の読み取りと書き込みを調整する必要があります。まずクチコミ自体を送信し、次にレストランの評価 countaverage rating を更新する必要があります。そのうちの 1 つが失敗し、もう 1 つが失敗すると、データベースのある部分のデータが別の部分のデータと一致しない、一貫性のない状態になります。

幸いなことに、Cloud Firestore にはトランザクション機能があり、単一のアトミック オペレーションで複数の読み取りと書き込みを実行できるため、データの整合性が維持されます。

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

FriendlyEats.Data.js

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);
    });
  });
};

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

12. データを保護する

この Codelab の冒頭で、あらゆる読み取りや書き込みに対してデータベースを完全に開くように、アプリのセキュリティ ルールを設定しました。実際のアプリケーションでは、望ましくないデータアクセスや変更を防ぐために、より詳細なルールを設定する必要があります。

  1. Firebase コンソールの [構築] セクションで、[Firestore データベース] をクリックします
  2. Cloud Firestore セクションの [ルール] タブをクリックします(またはここをクリックすると、ルールに直接移動できます)。
  3. デフォルトを以下のルールに置き換え、[公開] をクリックします。

firestore.rules

rules_version = '2';
service cloud.firestore {

  // Determine if the value of the field "key" is the same
  // before and after the request.
  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)
      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

13. まとめ

この Codelab では、Cloud Firestore を使用して基本的な読み取りと書き込みを行う方法と、セキュリティ ルールを使用してデータアクセスを保護する方法を学習しました。完全なソリューションについては、quickstarts-js リポジトリをご覧ください。

Cloud Firestore の詳細については、次のリソースをご覧ください。

14. (省略可)App Check で適用する

Firebase App Check は、アプリへの不要なトラフィックを検証、防止することで保護を提供します。このステップでは、reCAPTCHA Enterprise を使用して App Check を追加して、サービスへのアクセスを保護します。

まず、App Check と reCAPTCHA を有効にする必要があります。

reCAPTCHA Enterprise の有効化

  1. Cloud コンソールの [セキュリティ] で、[reCaptcha Enterprise] を見つけて選択します。
  2. プロンプトが表示されたらサービスを有効にし、[キーを作成] をクリックします。
  3. 指示に従って表示名を入力し、プラットフォーム タイプとして [ウェブサイト] を選択します。
  4. デプロイした URL を [ドメインリスト] に追加し、[チェックボックスによる本人確認を使用する] オプションがオフになっていることを確認します。
  5. [鍵を作成] をクリックし、生成された鍵を安全な場所に保存します。これはこのステップの後半で必要になります。

App Check を有効にする

  1. Firebase コンソールで、左側のパネルにある [ビルド] セクションを見つけます。
  2. [App Check] をクリックし、[開始] ボタンをクリックします(または コンソールに直接リダイレクトします)。
  3. [登録] をクリックし、画面の指示に従って reCaptcha Enterprise キーを入力し、[保存] をクリックします。
  4. API ビューで [ストレージ] を選択し、[適用] をクリックします。Cloud Firestore についても同じことを行います。

これで、App Check が適用されるようになりました。アプリを更新し、レストランを作成または表示してみます。次のエラー メッセージが表示されます。

Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.

App Check はデフォルトで、未検証のリクエストをブロックします。次に、アプリに検証を追加します。

FriendlyEats.View.js ファイルに移動して initAppCheck 関数を更新し、reCaptcha キーを追加して App Check を初期化します。

FriendlyEats.prototype.initAppCheck = function() {
    var appCheck = firebase.appCheck();
    appCheck.activate(
    new firebase.appCheck.ReCaptchaEnterpriseProvider(
      /* reCAPTCHA Enterprise site key */
    ),
    true // Set to true to allow auto-refresh.
  );
};

appCheck インスタンスは、キーを含む ReCaptchaEnterpriseProvider で初期化されます。isTokenAutoRefreshEnabled は、アプリ内のトークンの自動更新を許可します。

ローカルテストを有効にするには、FriendlyEats.js ファイルでアプリが初期化されているセクションを見つけて、FriendlyEats.prototype.initAppCheck 関数に次の行を追加します。

if(isLocalhost) {
  self.FIREBASE_APPCHECK_DEBUG_TOKEN = true;
}

これにより、ローカル ウェブアプリのコンソールに、次のようなデバッグ トークンがログに記録されます。

App Check debug token: 8DBDF614-649D-4D22-B0A3-6D489412838B. You will need to add it to your app's App Check settings in the Firebase console for it to work.

Firebase コンソールで App Check の [アプリビュー] に移動します。

オーバーフロー メニューをクリックして、[デバッグ トークンを管理] を選択します。

次に、[デバッグ トークンを追加] をクリックし、プロンプトに従ってコンソールからデバッグ トークンを貼り付けます。

これで完了です。これで、アプリで App Check が機能するようになりました。