AIイノベーションズ
Web開発

Firebaseの個人的ベストプラクティス

著者: AIイノベーションズ 阿部隼也X / Twitter

Firebaseを使えば、データベース、認証、ホスティング、分析などの機能を簡単に追加できます。

しかし、Firebaseのデータベース(Firestore)は、特有の「セキュリティルール」を用意する必要があったり、それ自体NoSQLであったりするためにそこまで普及していません。

そこで、この記事では、それでもこうやって使えばFirebaseは最高に便利だぞ、という個人的なベストプラクティスを紹介します。

データベース(Firestore)

※ 最近Firebaseに PostgreSQL が利用できるようになりましたので、その場合このFirestoreは関係ありません。 ※ さらに最近Firestoreは MongoDB 互換のAPIが利用できるようになりました。が、ここでは言及しません。

Firestoreの特徴

  • 最初から速い
  • データを管理画面UI上でCRUDできる
  • リアルタイムなデータ更新・表示がしやすい
  • NoSQL
  • APIサーバーがあるため、クライアントコード(SDK)でアクセスできる
  • クライアントコードでアクセスするならセキュリティルールが必須

Firestoreの厄介なところ

Firestoreの厄介なところは、NoSQLであり、さらにクライアントコードでアクセスするならセキュリティルールが必須であることです。

しかし、クライアントコードでアクセスできるのは非常に便利でコード量も減りますし、NoSQLだからといって特に難しいことはありません。

とはいえセキュリティルールは面倒です。なぜならFirestore独自のものであり、他のデータベースでは全く使わないものだからです。つまり学習が必要になります。

たとえば、users コレクションに対して自分だけがアクセスできるようにするには、以下のように書きます。

  match /users/{userId} {
    allow get, list: if request.auth.uid == userId; // = リクエストしてくるユーザーがuserId(FirestoreのドキュメントID)と一致している場合のみアクセスできる
  }

また、posts コレクションに対して誰でもアクセスできるようにするには、以下のように書きます。

  match /posts/{postId} {
    allow get, list: true; // = 常に誰でもアクセスできる
  }

一見シンプルですが、「あるデータを共有して複数ユーザーが閲覧できるようにしたい」といった場合など、少し複雑になるだけで頭を抱えるほど悩みます(だいたい詰みます)。

セキュリティルールのベストプラクティス

そこで以下のようにセキュリティルールを書いていくと非常にラクになります。

データ構造の設計はセキュリティルールありきで考える

ある程度セキュリティルールを書いて問題ないことを確認したうえでデータ構造を用意していってください。

そうしないと、あとになってセキュリティルールが書けないという問題が置きます。

たとえば、コレクションの中の一部のフィールドについてのみルールを適用したい場合がありますが、それは仕様上不可能です。

積極的にコレクションを作る

セキュリティルールは万能ではないし、想定外のデータを扱うことになります。

そこで、たとえばコレクションの中の一部のフィールドについてのみルールを適用したい場合がありますが、それは仕様上不可能。

なので、新たにコレクションを作成して、その中にデータを入れておくという方法があります。

たとえば、users コレクションに対して自分だけがアクセスできるようにするには、以下のように書きます。

  match /users/{userId} {
    allow get, list: if request.auth.uid == userId;
  }

一方で、user_profiles コレクションに対しては、誰でもアクセスできるようにするには、以下のように書きます。

  match /user_profiles/{userId} {
    allow get, list: true;
  }

同時に、user_settings コレクションに対しては、サーバー(Firebase Admin SDKなど)のみでアクセスできるようにするには、以下のように書きます。

  match /user_settings/{userId} {
    allow get, list: if false; // = 常にクライアントコードではアクセスできない
  }

このように、コレクションを新たに作成して、その中にデータを入れておくという方法があります。

セキュリティルールはコレクションごとに適用するものなので、このようにガンガンコレクションを作っていけばいいのです。

1つのコレクションの中に全てのユーザー情報を入れるのではなく、それぞれの種類の情報はそれぞれのコレクションに入れておくというのも一つの方法なのです。

FirestoreはNoSQLですから、そのあたりもしやすいのです。

${userId}_${postId} のようなドキュメントIDを使う

ドキュメントIDは、複数の関連ドキュメントIDを組み合わせて作成するというのも一つの方法です。

  const newDocumentId = `${userId}_${postId}`;

このように書けば、セキュリティルールを非常に適用しやすいためです。

たとえば、

id, created_at, updated_at を全てのドキュメントのフィールドに入れる

created_at, updated_at はFirestoreに限らずという感じで説明不要かと思いますが、 idについては実はFirestoreはuidという別情報があるので、さらにフィールドとしてidを追加するのは冗長になってしまうのですが、uidを使うのはあまりにも面倒なので、フィールドにも入れておきます。

uidだけで管理すると、管理画面やデータ取得時にドキュメントを見るときに、uidがわかりにくいのです。ドキュメントを取得したとき、idがあれば取得しやすいですしコードも見やすくなります。

ドキュメントIDは手動生成する

FirestoreにはドキュメントIDを自動生成させる方法( addDoc() )がありますが、それだと下記のようにIDとデータが別々になってしまいます。

const db = getFirestore(app);

// ドキュメント作成
const data = {
  title: "Hello world",
  content: "This is a test post",
};
const documentRef = await addDoc(collection(db, "posts"), data);
console.log("ドキュメントID: ", documentRef.id);
console.log("ドキュメントデータ: ", documentRef.data());

そこで、以下のようにドキュメントIDを手動で生成します。

const db = getFirestore(app);

// ドキュメント作成
const newDocumentId = crypto.randomUUID(); //たとえばUUIDを生成する
const data = {
  id: newDocumentId,
  title: "Hello world",
  content: "This is a test post",
};
await setDoc(doc(db, "posts", newDocumentId), data);
console.log("ドキュメントデータ: ", data);

PR