【Firebase】Firestoreから上手いことデータを取得する

公式のドキュメントを見れば大体の使い方はわかりますが、ベストプラクティスを考えると結構深いところまで考える必要があります。今回は、既存の機能をFirestoreを使って改修していきます。

2024年04月03日
関連記事

仲間内で共有するメモ的に書いていきます。
公式のドキュメントだけだと、ぱっと共通化のイメージが湧かないので、この記事でまとめてみようと思います。

単一のドキュメント取得(公式)

以下のソースは公式からパクってきたやつです。
コレクションが増えるにつれ、同じような内容が増えてきてしまうのがネックですね。

なんとかCRUDを共通化したい。

import { doc, getDoc } from "firebase/firestore";

const docRef = doc(db, "cities", "SF");
const docSnap = await getDoc(docRef);

if (docSnap.exists()) {
  console.log("Document data:", docSnap.data());
} else {
  // docSnap.data() will be undefined in this case
  console.log("No such document!");
}

コレクションごとにクラスを

以前、【Nuxt3】クライアントサイドからエラーページへ飛ばす方法 という記事を書きました。

野菜の名前で動的ルーティングを作って表示するだけの簡単なものです。
変数にデータを突っ込んでいただけなので、Firestoreで改修していきます。

まず、野菜コレクション用のクラスとして、models/Vegetable.ts を作成します。

import { ModelBase, type IModelBase } from './ModelBase'

export interface IVegetable extends IModelBase {
  name: string
}
export class Vegetable extends ModelBase implements IVegetable {
  static readonly MODEL_NAME: string = 'vegetables'
  name: string

  constructor(values: Partial<IVegetable>) {
    super(values)
    this.name = values?.name || ''
  }
}

こいつをインスタンス化して、Firestoreのドキュメントを格納していく流れです。
ModelBaseには、id, created_at, updated_atを設定し、すべてのドキュメント共通のコレクションIDとフィールドを持っておきます。

  • MODEL_NAME: コレクション名となる
  • constructor: ここでドキュメントの各フィールドをクラス変数に当てこんでいく

Firestoreとのやりとり

まず、Firestoreが扱える環境にあることを確認しましょう。
plugins から、useNuxtApp().$dbのようにして接続情報の取得が可能になっています。

先ほど、Vegetableクラスを用意しました。
このクラスを型ジェネリックして、FirestoreのCRUDを共通化していきます。

CRUD

Firestoreからデータを取得したり、保存したりする際に必要な処理を一つのテンプレートにまとめて、どのコレクションに対しても同じ方法で操作ができるようにしています。

ここに getaddremoveall などを書いていくイメージ。
野菜の名前で検索したいだけなので selectByQueryのみを実装しています。

import {
  Firestore,
  QueryDocumentSnapshot,
  DocumentData,
  getDocs,
  Query,
  CollectionReference,
  collection,
} from 'firebase/firestore'

export namespace FirestoreUtil {
  export abstract class Model<T> {
    abstract get modelName(): string
    abstract docToModel(doc: QueryDocumentSnapshot | DocumentData): T

    get db(): Firestore {
      return useNuxtApp().$db
    }

    getCollectionRef(): CollectionReference {
      return collection(this.db, this.modelName)
    }

    protected async selectByQuery(q: Query): Promise<T[]> {
      const querySnapshot = await getDocs(q)
      const list: T[] = []
      querySnapshot.forEach((doc) => {
        list.push(this.docToModel(doc))
      })
      return list
    }
  }

  export function toD<T>(doc: QueryDocumentSnapshot | DocumentData): T {
    return Object.assign({}, doc.data(), { id: doc.id }) as T
  }
}

※ getDocsやgetDocなどの戻り値は Firestoreの型になるので、非常に使いにくい。toDでプロジェクト内で使いやすい型に変えてあげよう。

コレクション別に用意

特定のフィールドで絞り込んだり、コレクション名やコンストラクタの導線を持たせたりと、独自の処理を書いていきます。

import { FirestoreUtil } from './FirestoreUtil';
import {
  DocumentData,
  QueryDocumentSnapshot,
  query,
  where
} from 'firebase/firestore';
import {
  Vegetable,
  type IVegetable
} from '@/models/Vegetable';

export class VegetableUtil extends FirestoreUtil.Model<Vegetable> {
  get modelName(): string {
    return Vegetable.MODEL_NAME
  }

  docToModel(doc: QueryDocumentSnapshot | DocumentData): Vegetable {
    const docD = FirestoreUtil.toD<IVegetable>(doc)
    return new Vegetable(docD)
  }

  async getByName(name: string): Promise<Vegetable | null> {
    const q = query(super.getCollectionRef(), where('name', '==', name))

    const list = await super.selectByQuery(q)
    return list[0] || null
  }
}

使い方と共有

使い方は簡単。

const vegetableUtil = new VegetableUtil()
const path = useRoute().params._vegetable as string
const target = await vegetableUtil.getByName(path)

ルールも書こう。

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /vegetables/{vegetableId} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}

※ 適当に書いたので、用途に合わせて

これが今回のブランチ。
うちのエンジニアたちは、どんな内容でも興味深く聞いてくれるので書いてみたものの、まじで共有する程の内容じゃなかったかもしれない。

筆者情報
IT業界経験6年目のフルスタックエンジニア。
フロントエンドを軸として技術を研鑽中でございます。