Firebase と添い遂げる Advent Calendar 2024、3日目です。
Firestore はスキーマレス
Firestoreに対する批判の一つにスキーマレスというのがある。 スキーマレスだとデータベースの中身は実際に問い合わせないと分からないということで嫌われていると思うのだが、一方で自分はスキーマレスがFirestoreの良いところでもあると思っている。 これから何回かに分けて安全な Firestore について書いていくので、今日はスキーマレスについて整理しておきたい。
スキーマレスの何が悪か
やはりデータベースの中身は実際に問い合わせないと分からないところだろう。 スキーマが分からないから、そのDBに何が入るのか分からない。 型だけでなく、コメントもないからカラムやフィールドの意味も分からない。
それに複数のDBクライアントからちぐはぐなデータを挿入することも可能だ。 その場合、読み取ったデータ次第で nullぽを引き起こすことも容易だ。 これはチーム開発では致命的だ。
スキーマレスであることの嬉しさ
一方でスキーマがないことの嬉しさもある。 まず、型の生成が不要であることだ。 現代的なプログラミング言語やライブラリでは、型推論を手に入れるために、スキーマ情報からクライアントで利用できる型情報を生成する。 これはビルドに1ステップ挟まり困難が生まれることもある。
次にマイグレーションが不要であるということだ。 型情報を作るためにはスキーマが必要となるが、このスキーマをDB上に作るためにはマイグレーションという操作が必要になる。 これもビルドに1ステップが挟まる。
ビルドに1ステップ挟まるだけなら、一度環境を作ればそれで終いと思うかもしれないが、これは設計を歪ませることもある。 マイグレーションがビルドの1ステップになるということにより、アプリケーションとデータベースが結合してしまう。 つまりデータベースを構築するためにはアプリケーションが必要という状況が生まれてしまう。 その結果、1アプリに対して1DBというのが適した設計となってしまう。 本来 DB は独立していて複数のアプリケーションから接続されても良いはずである。 これは例えば Prisma を素直に使っていたりすると出会う問題ではある。
もちろんこの問題は運用次第で解消はできる。 例えば DB のスキーマやマイグレーションファイルだけを別リポジトリやフォルダで管理しておくことだ。 そしてアプリケーションはマイグレーションが適用されたDBを直接参照して、そこから型安全なDBクライアントを生成する方法がある。 例えば sqlx はそれを "compile-time checked queries without a DSL" と銘打っており、現実にあるソリューションだ。 しかしこれもこれで、アプリケーションのビルド前に別レポジトリのマイグレーションを済ませておかないといけないという人力オペレーションが発生し、別レポジトリに跨っている場合はその依存関係の解消は厄介だ。 それは単一の CI/CD では守れないので開発者体験は損なっているかもしれない。 また、カラムの削除をした場合などは、その修正をしたアプリケーションをデプロイするまでは nullぽが起きるので、長めのダウンタイムをケアしないといけないという問題もある。 いずれにせよ、ビルドの依存関係を解決しないといけないので、ちょっと大変ではある。
この手の問題はスキーマレスのDBであれば起きないのである。 なぜならマイグレーションがないからだ。 これは 1DB で複数アプリを接続する場合でもかなり強力である。 実際、FirestoreはWebクライアント、ネイティブアプリクライアント、Adminクライアントが存在しておりそれらを同時にアクセスさせることを想定している設計になっている。
デメリットを打ち消しながら Firestore を使いたい
さて、Firestoreは複数クライアントからアクセスするのに適しているという話をしたが、依然として複数のDBクライアントからちぐはぐなデータを挿入される問題は残る。 その解決方法としてはアプリケーション側でスキーマを持ってしまえば良いというのがある。 firestore には converter という仕組みがあり、Firestoreへの挿入と、Firestoreからの取得時に処理をHookする機能がある。 4年前その解説ブログを書いたこともあるので詳しくはそちらを見て欲しいが、要は挿入や取得に対して型検査やvalidationをやれるということである。 この機能があれば、特定の型を持ったレコードしか挿入できないようにできる。 そのため collection ごとにそれを実現する firestore client を用意しておき、その client を通してのみ DB を操作するとすれば、converter の型情報がDBスキーマ情報として扱えるようになる。 複数サービスからDBを操作する場合は、その firestore client を npm パッケージとして配布すれば良い。 そうするとスキーマレスのデメリットを打ち消しながら Firestore を使える。