Firestoreをもっと手軽に使えるfirestore-simpleを作った
追記: v2についてのエントリはこちら
Firebaseの新しいDBであるFirestoreは使っているでしょうか?
今まではどうしてもサーバー側の実装が必要だったDBへのデータの格納がクライアントだけで済むようになったので圧倒的に便利ですね。
そんなわけで自分もjsからFirestoreを使う機会が増えてきたのですが、毎回書くたびにAPIが微妙に使いづらいと感じていました。そこで、Firestoreをいい感じに扱えるfirestore-simpleというライブラリをリリースしました。
- npm: https://www.npmjs.com/package/firestore-simple
- GitHub: https://github.com/Kesin11/Firestore-simple
サンプルコード
まず、比較のためにFirestoreを素で使った場合のコードです。
const admin = require('firebase-admin') const serviceAccount = require('./firebase_secret.javascripton') admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }) const main = async () => { const firestore = admin.firestore() const collectionPath = 'users' await firestore.collection(collectionPath).add({ name: 'alice', age: 20 }) await firestore.collection(collectionPath).add({ name: 'bob', age: 22 }) const user_documents = await firestore.collection(collectionPath).get() const users = [] user_documents.forEach((document) => { const data = document.data() users.push({ id: document.id, name: data.name, age: data.age }) }) // [ { id: 'lZ9HtQAEJvumL9grjGtc', name: 'bob', age: 22 }, // { id: 'rFx0a6dZQZPIHEvXGGtO', name: 'alice', age: 20 } ] const batch = firestore.batch() users.forEach((user) => { const documentRef = firestore.collection(collectionPath).doc(user.id) batch.delete(documentRef) }) await batch.commit() } main()
そしてこちらがfirestore-simpleを使った場合のコードです。
const admin = require('firebase-admin') const { FirestoreSimple } = require ('firestore-simple') const serviceAccount = require('./firebase_secret.json') admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }) const main = async () => { const firestore = admin.firestore() const collectionPath = 'users' const dao = new FirestoreSimple(firestore, collectionPath) await dao.add({ name: 'alice', age: 20}) await dao.add({ name: 'bob', age: 22 }) const users = await dao.fetchCollection() // [ { id: '3mBYA0uXw0lw6b883jgk', age: 20, name: 'alice' }, // { id: 'lE9SfP8f3H8w0Uwk0euQ', age: 22, name: 'bob' } ] const ids = users.map((user) => user.id) await dao.bulkDelete(ids) } main()
どうでしょうか。素のFirestoreに比べて短く、スッキリとしたコードで書けると思います。
firestone-simpleの特徴
1インスタンス1コレクション
素のFirestoreでは例えばuser
にアクセスする場合、firestore.collection(‘user’)
と書く必要があります。コードの各所からuser
のドキュメントにアクセスしたい場合、毎回同じコードを書くのはイヤなので自分ならUserDaoとかUserModelみたいなクラスを作り、そのクラス経由でアクセスするようにしたいです。
firestore-simpleはコンストラクタにcollectionPathを渡してインスタンスを作ります。1インスタンス1コレクションという制約を設ける代わりに、collectionPathを指定するのは最初の1回だけで済みます。
普通のObjectを返す
Firestoreのドキュメントを取得するとき、得られるのはDocumentSnapshotなのでidを参照するにはdoc.id
、それ以外の要素にアクセスするにはdoc.data()
が必要です。
そのため、{title: ‘test’}
というドキュメントをFirestoreから取り出すときには、いちいちdoc.data().title
とする必要があり地味に面倒くさいです。
firestore-simpleはfetchDocument
、fetchCollection
の参照系だけではなくadd
やset
といった更新系も基本的にObjectを返します。このObjectにはドキュメントのデータに加えてid
も追加されているためdoc.id
、doc.title
というようにアクセスすることが可能です。
普通のArrayを返す
Firestoreでコレクションから複数のドキュメントを取得する場合、得られるのはQuerySnapshotです。ドキュメントのサンプルコードではforEach
を使っているのでmap
も使えると思いきや使えません。どうもQuerySnapshotのリファレンスを見る限りではforEach
しか実装されていないようで、map
やfilter
が使えないので大変不便です。
firestore-simpleではfetchCollection
、fetchByQuery
といった複数のドキュメントを取得するメソッドはArrayを返します。従ってmap
も普通に使うことが可能です。
キーの別名マッピングに対応
jsに限らずJSONでデータをやりとりするAPIのクライアントを書くときに地味に面倒くさいのが、キーの名前をsnake_caseやCamelCaseといったクライアント側の言語の流儀に合わせて変換することです。 この変換処理をコードの色々なところで繰り返したくないので、大抵はAPIクライアントかModelクラスにJSONのキーとのマッピングを書くことになると思います。
大規模なコードであればFirestoreのコレクションと1対1で対応するModelクラスを用意してもいいのですが、そこまでしたくない場合のためにfirestore-simpleではキーの別名マッピングに対応しています。
const dao = new FirestoreSimple(firestore, collectionPath, { mapping: { createdAt: "created_at" }}) await dao.add({ name: 'alice', age: 20, createdAt: new Date() })
このようにすると、js側ではcreatedAt
というキーで扱いますが、Firestore側のドキュメントではcreated_atというキーで保存されます。
メソッド
参照系
fetchDocument
1つのドキュメントをフェッチします。
await dao.fetchDocument(documentId)
fetchCollectoin
コレクション全てのドキュメントをフェッチします。
await dao.fetchCollection()
fetchByQuery
where
, orderBy
, limit
などを使ってコレクションから絞りこんだりソートしてフェッチします。
const query = dao.collectionRef .where('name', '==', 'alice') .orderBy('age') .limit(1) await dao.fetchByQuery(query)
更新系
add
idを自動採番でFirestoreにドキュメントを追加します。
await dao.add({ name: 'alice', age: 20 })
set
引数のObjectのid
に対応するFirestore側のドキュメントを更新します。
そのidに対応するドキュメントがまだFirestoreに存在しない場合はそのidで追加されるので、add
による自動採番ではなく任意のidで登録したい場合もset
を使います。
await dao.set({ id: '1111', name: 'bob', age: 22})
addOrSet
引数のオブジェクトにid
が含まれていればset
、含まれなければadd
と同じ挙動になります。
元々のFirestoreには存在しないfirestore-simpleのオリジナル機能です。
await dao.addOrSet({ name: 'alice', age: 20 }) // add await dao.setOrSet({ id: '1111', name: 'bob', age: 22}) // set
delete
引数のidに対応したドキュメントをFirestoreから削除します。
await dao.delete(documentId)
Bulk系
ドキュメントで一括書き込みと書かれている処理です。複数のドキュメントへの処理をアトミックに行いたい場合は使用します。
bulkSet
引数に渡した複数のオブジェクトでFirestoreのドキュメントを一括で更新します。
await dao.bulkSet([ { id: '1111', name: 'alice', age: 20 }, { id: '2222', name: 'bob', age: 22 } ])
bulkDelete
引数に渡した複数のidでFirestoreのドキュメントを一括で削除します。
await dao.bulkDelete(['1111', '2222'])
できないこと
update
Firestoreにはドキュメント更新の方法としてset
以外にupdate
も存在しますが、自分はupdate
を使う必要性を感じなかったので実装していないです。
トランザクション
自分が使いたい用途としてはbulkSet
、bulkDelete
でほぼ十分だったことと、トランザクションをサポートするには実装が複雑になってしまうことから見送りました。
onSnapshot()
これも自分が今の所使う必要がなかったため未サポートです。
ただ、firestore-simpleのインスタンスからcollectionRefにアクセス可能なので、このようなコードでonSnapshot()
を使うことは可能です。
dao.collectionRef.onSnapshot((snapshot) => { snapshot.docChanges.forEach((change) =>{ console.log(change.type) console.log(change.doc.data()) }) })
ReactNative
firestore-simpleはReactNativeFirebaseでも使用可能です。
ReactNativeFirebaseではfirestoreのインスタンスをゲットする方法が多少異なりますが、それ以後の使い方は全く同じです。
import { FirestoreSimple } from 'firestore-simple' import firebase from 'react-native-firebase'; export default class App extends React.Component { // signInAnonymouslyAndRetrieveDataを使う場合はfirebaseのコンソールで匿名ログインをオンにする必要があります async getFirestore() { await firebase.auth().signInAnonymouslyAndRetrieveData() return firebase.firestore() } async componentDidMount() { const firestore = await this.getFirestore() const collectionPath = 'sample_react_native' const dao = new FirestoreSimple(firestore, collectionPath, { mapping: { createdAt: 'created_at' } }) // ... other usage are same as nodejs client } }
追記6/29: CloudFunction
CloudFunctionでも問題なく使えることを確認しました
const functions = require('firebase-functions'); const admin = require('firebase-admin') const { FirestoreSimple } = require('firestore-simple') admin.initializeApp() exports.helloFirestore = functions.https.onRequest((request, response) => { const firestore = admin.firestore() const dao = new FirestoreSimple(firestore, 'cloud_function') dao.add({ name: 'alice', age: 20}).then(doc => { console.log(doc) return response.send("add { name: " + doc.name + ", age: " + doc.age + "}") }) })
TypeScript
firestore-simpleはTypeScriptで書かれているため、型定義も同梱されています。
逆にflowには対応していません。
まとめ
Firestoreをもっと手軽に扱えるfirestore-simpleを作りました。
簡単な用途ならfirestore-simpleを素のまま使っても便利だと思いますが、firestore-simpleを組み込むか、継承することでより重厚なFirestoreクライアントを自作したいというニーズにも応えられると思います。
自分はオリジナルのFirestoreのAPIを忘れてしまうぐらい便利に使いまくっているので、ぜひ使ってみてください。