新しいFirebaseデータベースであるCloud Firestoreを使用して、コレクションのアイテム数をカウントすることはできますか?
もしそうなら、どうすればいいですか?
新しいFirebaseデータベースであるCloud Firestoreを使用して、コレクションのアイテム数をカウントすることはできますか?
もしそうなら、どうすればいいですか?
回答:
多くの質問と同様に、答えは-場合によります。
フロントエンドで大量のデータを処理する場合は、十分に注意する必要があります。Firestoreは、フロントエンドの反応が鈍いことに加えて、100万回の読み取りごとに$ 0.60を請求します。
注意して使用してください-フロントエンドのユーザーエクスペリエンスが影響を受ける可能性があります
この返された配列であまりロジックを実行しない限り、フロントエンドでこれを処理しても問題ありません。
db.collection('...').get().then(snap => {
size = snap.size // will return the collection size
});
注意して使用してください-Firestoreの読み取り呼び出しにはかなりのコストがかかる場合があります
フロントエンドでこれを処理することは、ユーザーシステムの速度を低下させる可能性が高すぎるため、現実的ではありません。このロジックサーバー側を処理し、サイズのみを返す必要があります。
このメソッドの欠点は、Firestoreの読み取り(コレクションのサイズに等しい)がまだ呼び出されているため、長期的には予想以上にコストがかかる可能性があることです。
クラウド機能:
...
db.collection('...').get().then(snap => {
res.status(200).send({length: snap.size});
});
フロントエンド:
yourHttpClient.post(yourCloudFunctionUrl).toPromise().then(snap => {
size = snap.length // will return the collection size
})
最もスケーラブルなソリューション
FieldValue.increment()
2019年4月の時点で、Firestoreは完全にアトミックに、以前のデータを読み取らずにカウンターをインクリメントできるようになりました。これにより、複数のソースから同時に更新する場合でも(以前はトランザクションを使用して解決されました)、正しいカウンター値が得られると同時に、実行するデータベース読み取りの数も削減されます。
ドキュメントの削除または作成をリッスンすることにより、データベースにあるカウントフィールドに追加または削除できます。
firestore docs- Distributed Countersを参照する か、Jeff Delaneyによるデータ集約をご覧ください。彼のガイドは、AngularFireを使用するすべての人にとって本当に素晴らしいですが、彼のレッスンは他のフレームワークにも引き継がれるはずです。
クラウド機能:
export const documentWriteListener =
functions.firestore.document('collection/{documentUid}')
.onWrite((change, context) => {
if (!change.before.exists) {
// New document Created : add one to count
db.doc(docRef).update({numberOfDocs: FieldValue.increment(1)});
} else if (change.before.exists && change.after.exists) {
// Updating existing document : Do nothing
} else if (!change.after.exists) {
// Deleting document : subtract one from count
db.doc(docRef).update({numberOfDocs: FieldValue.increment(-1)});
}
return;
});
これで、フロントエンドでこのnumberOfDocsフィールドをクエリして、コレクションのサイズを取得できます。
firestore.runTransaction { ... }
ブロックでラップする必要があることを追加したいと思います。これにより、へのアクセスに関する同時実行の問題が修正されますnumberOfDocs
。
これを行う最も簡単な方法は、「querySnapshot」のサイズを読み取ることです。
db.collection("cities").get().then(function(querySnapshot) {
console.log(querySnapshot.size);
});
「querySnapshot」内のdocs配列の長さを読み取ることもできます。
querySnapshot.docs.length;
または、「querySnapshot」が空の値を読み取って空の場合、ブール値が返されます。
querySnapshot.empty;
db.collection.count()
。これだけのためにそれらを落とすことを考えています
私の知る限り、このための組み込みソリューションはなく、現在はノードSDKでのみ可能です。あなたが持っている場合
db.collection('someCollection')
あなたは使うことができます
.select([fields])
選択するフィールドを定義します。空のselect()を実行すると、ドキュメント参照の配列が取得されます。
例:
db.collection('someCollection').select().get().then(
(snapshot) => console.log(snapshot.docs.length)
);
このソリューションは、すべてのドキュメントをダウンロードするという最悪の場合にのみ最適化されており、大規模なコレクションには対応していません。
これもご覧ください:
Cloud Firestoreを使用してコレクション内のドキュメント数を取得する方法
select(['_id'])
は、より高速ですselect()
大規模なコレクションの場合は、ドキュメントの数を数えるのに注意してください。すべてのコレクションに対して事前に計算されたカウンターが必要な場合は、firestoreデータベースでは少し複雑です。
この場合、このようなコードは機能しません。
export const customerCounterListener =
functions.firestore.document('customers/{customerId}')
.onWrite((change, context) => {
// on create
if (!change.before.exists && change.after.exists) {
return firestore
.collection('metadatas')
.doc('customers')
.get()
.then(docSnap =>
docSnap.ref.set({
count: docSnap.data().count + 1
}))
// on delete
} else if (change.before.exists && !change.after.exists) {
return firestore
.collection('metadatas')
.doc('customers')
.get()
.then(docSnap =>
docSnap.ref.set({
count: docSnap.data().count - 1
}))
}
return null;
});
その理由は、Firestoreのドキュメントにあるように、すべてのクラウドFirestoreトリガーをべき等にする必要があるためです:https : //firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees
したがって、コードの複数の実行を防ぐために、イベントとトランザクションで管理する必要があります。これは、大きなコレクションカウンターを処理するための私の特別な方法です。
const executeOnce = (change, context, task) => {
const eventRef = firestore.collection('events').doc(context.eventId);
return firestore.runTransaction(t =>
t
.get(eventRef)
.then(docSnap => (docSnap.exists ? null : task(t)))
.then(() => t.set(eventRef, { processed: true }))
);
};
const documentCounter = collectionName => (change, context) =>
executeOnce(change, context, t => {
// on create
if (!change.before.exists && change.after.exists) {
return t
.get(firestore.collection('metadatas')
.doc(collectionName))
.then(docSnap =>
t.set(docSnap.ref, {
count: ((docSnap.data() && docSnap.data().count) || 0) + 1
}));
// on delete
} else if (change.before.exists && !change.after.exists) {
return t
.get(firestore.collection('metadatas')
.doc(collectionName))
.then(docSnap =>
t.set(docSnap.ref, {
count: docSnap.data().count - 1
}));
}
return null;
});
ここでの使用例:
/**
* Count documents in articles collection.
*/
exports.articlesCounter = functions.firestore
.document('articles/{id}')
.onWrite(documentCounter('articles'));
/**
* Count documents in customers collection.
*/
exports.customersCounter = functions.firestore
.document('customers/{id}')
.onWrite(documentCounter('customers'));
ご覧のとおり、複数の実行を防ぐための鍵は、コンテキストオブジェクトのeventIdというプロパティです。関数が同じイベントで何度も処理された場合、イベントIDはすべての場合で同じになります。残念ながら、データベースには「イベント」コレクションが必要です。
context.eventId
同じトリガーの複数の呼び出しで常に同じであることを確認できますか?私のテストではそれは一貫しているように見えますが、これを述べている「公式」のドキュメントを見つけることができません。
2020年には、Firebase SDKではまだ使用できませんが、Firebase Extensions(ベータ)では使用できますが、セットアップと使用はかなり複雑です...
合理的なアプローチ
ヘルパー...(作成/削除は冗長に見えますが、onUpdateよりも安価です)
export const onCreateCounter = () => async (
change,
context
) => {
const collectionPath = change.ref.parent.path;
const statsDoc = db.doc("counters/" + collectionPath);
const countDoc = {};
countDoc["count"] = admin.firestore.FieldValue.increment(1);
await statsDoc.set(countDoc, { merge: true });
};
export const onDeleteCounter = () => async (
change,
context
) => {
const collectionPath = change.ref.parent.path;
const statsDoc = db.doc("counters/" + collectionPath);
const countDoc = {};
countDoc["count"] = admin.firestore.FieldValue.increment(-1);
await statsDoc.set(countDoc, { merge: true });
};
export interface CounterPath {
watch: string;
name: string;
}
エクスポートされたFirestoreフック
export const Counters: CounterPath[] = [
{
name: "count_buildings",
watch: "buildings/{id2}"
},
{
name: "count_buildings_subcollections",
watch: "buildings/{id2}/{id3}/{id4}"
}
];
Counters.forEach(item => {
exports[item.name + '_create'] = functions.firestore
.document(item.watch)
.onCreate(onCreateCounter());
exports[item.name + '_delete'] = functions.firestore
.document(item.watch)
.onDelete(onDeleteCounter());
});
実行中
建物のルートコレクションとすべてのサブコレクションが追跡されます。
ここで/counters/
ルートパスの下
これで、コレクション数は自動的にそして最終的に更新されます!カウントが必要な場合は、コレクションパスを使用し、プレフィックスとしてを付けcounters
ます。
const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const collectionCount = await db
.doc('counters/' + collectionPath)
.get()
.then(snap => snap.get('count'));
@Matthewに同意します。このようなクエリを実行するとコストが高くなります。
[プロジェクトを開始する前の開発者へのアドバイス]
最初はこの状況を予測していたので、実際に、ドキュメントを使用してカウンターを収集し、typeを持つフィールドにすべてのカウンターを格納できますnumber
。
例えば:
コレクションのCRUD操作ごとに、カウンタードキュメントを更新します。
次回、コレクションの数を取得する場合は、ドキュメントフィールドをクエリまたはポイントするだけです。[1回の読み取り操作]
さらに、コレクション名を配列に格納できますが、これはトリッキーです。firebaseでの配列の状態は次のように表示されます。
// we send this
['a', 'b', 'c', 'd', 'e']
// Firebase stores this
{0: 'a', 1: 'b', 2: 'c', 3: 'd', 4: 'e'}
// since the keys are numeric and sequential,
// if we query the data, we get this
['a', 'b', 'c', 'd', 'e']
// however, if we then delete a, b, and d,
// they are no longer mostly sequential, so
// we do not get back an array
{2: 'c', 4: 'e'}
したがって、コレクションを削除しない場合は、毎回すべてのコレクションをクエリする代わりに、実際に配列を使用してコレクション名のリストを保存できます。
それが役に立てば幸い!
いいえ、現在、集計クエリの組み込みサポートはありません。しかし、あなたができることがいくつかあります。
最初はここに文書化されています。トランザクションまたはクラウド機能を使用して、集計情報を維持できます。
この例は、関数を使用して、サブコレクション内の評価の数と平均評価を追跡する方法を示しています。
exports.aggregateRatings = firestore
.document('restaurants/{restId}/ratings/{ratingId}')
.onWrite(event => {
// Get value of the newly added rating
var ratingVal = event.data.get('rating');
// Get a reference to the restaurant
var restRef = db.collection('restaurants').document(event.params.restId);
// Update aggregations in a transaction
return db.transaction(transaction => {
return transaction.get(restRef).then(restDoc => {
// Compute new number of ratings
var newNumRatings = restDoc.data('numRatings') + 1;
// Compute new average rating
var oldRatingTotal = restDoc.data('avgRating') * restDoc.data('numRatings');
var newAvgRating = (oldRatingTotal + ratingVal) / newNumRatings;
// Update restaurant info
return transaction.update(restRef, {
avgRating: newAvgRating,
numRatings: newNumRatings
});
});
});
});
jbbが言及した解決策は、ドキュメントをたまにしか数えたくない場合にも役立ちます。select()
ステートメントを使用して、各ドキュメントをすべてダウンロードしないようにしてください(カウントのみが必要な場合は、帯域幅が大量になります)。 select()
現時点ではサーバーSDKでのみ利用できるため、モバイルアプリではソリューションは機能しません。
直接利用できるオプションはありません。あなたはできませんdb.collection("CollectionName").count()
。以下は、コレクション内のドキュメント数のカウントを確認する2つの方法です。
db.collection("CollectionName").get().subscribe(doc=>{
console.log(doc.size)
})
上記のコードを使用すると、ドキュメントの読み取りはコレクション内のドキュメントのサイズに等しくなり、上記のソリューションの使用を避ける必要があるのはそのためです。
db.collection("CollectionName").doc("counts")get().subscribe(doc=>{
console.log(doc.count)
})
上記では、すべてのカウント情報を保存するために名前がカウントされたドキュメントを作成しました。カウントドキュメントは次の方法で更新できます。
wrt価格(Document Read = 1)と高速データ検索の上記のソリューションは適切です。
admin.firestore.FieldValue.incrementを使用してカウンターを増分します。
exports.onInstanceCreate = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
.onCreate((snap, context) =>
db.collection('projects').doc(context.params.projectId).update({
instanceCount: admin.firestore.FieldValue.increment(1),
})
);
exports.onInstanceDelete = functions.firestore.document('projects/{projectId}/instances/{instanceId}')
.onDelete((snap, context) =>
db.collection('projects').doc(context.params.projectId).update({
instanceCount: admin.firestore.FieldValue.increment(-1),
})
);
この例ではinstanceCount
、ドキュメントがinstances
サブコレクションに追加されるたびに、プロジェクトのフィールドを増分します。フィールドがまだ存在しない場合は作成され、1に増分されます。
インクリメントは内部的にトランザクションですが、1秒よりも頻繁にインクリメントする必要がある場合は、分散カウンターを使用する必要があります。
これは、実装することはしばしば望ましいですonCreate
とonDelete
いうよりonWrite
、あなたが呼び出すとonWrite
(あなたがあなたのコレクション内のドキュメントを更新する場合)は、不要な関数呼び出しでより多くのお金を費やしていることを意味するアップデートについて。
これらすべてのアイデアを使用して、すべてのカウンター状況(クエリを除く)を処理するユニバーサル関数を作成しました。
唯一の例外は、1秒間に非常に多くの書き込みを行う場合で、速度が低下します。例は次のようになり同類のトレンドポストに。たとえば、ブログ投稿ではやり過ぎであり、コストが高くなります。その場合は、シャードを使用して別の関数を作成することをお勧めします:https : //firebase.google.com/docs/firestore/solutions/counters
// trigger collections
exports.myFunction = functions.firestore
.document('{colId}/{docId}')
.onWrite(async (change: any, context: any) => {
return runCounter(change, context);
});
// trigger sub-collections
exports.mySubFunction = functions.firestore
.document('{colId}/{docId}/{subColId}/{subDocId}')
.onWrite(async (change: any, context: any) => {
return runCounter(change, context);
});
// add change the count
const runCounter = async function (change: any, context: any) {
const col = context.params.colId;
const eventsDoc = '_events';
const countersDoc = '_counters';
// ignore helper collections
if (col.startsWith('_')) {
return null;
}
// simplify event types
const createDoc = change.after.exists && !change.before.exists;
const updateDoc = change.before.exists && change.after.exists;
if (updateDoc) {
return null;
}
// check for sub collection
const isSubCol = context.params.subDocId;
const parentDoc = `${countersDoc}/${context.params.colId}`;
const countDoc = isSubCol
? `${parentDoc}/${context.params.docId}/${context.params.subColId}`
: `${parentDoc}`;
// collection references
const countRef = db.doc(countDoc);
const countSnap = await countRef.get();
// increment size if doc exists
if (countSnap.exists) {
// createDoc or deleteDoc
const n = createDoc ? 1 : -1;
const i = admin.firestore.FieldValue.increment(n);
// create event for accurate increment
const eventRef = db.doc(`${eventsDoc}/${context.eventId}`);
return db.runTransaction(async (t: any): Promise<any> => {
const eventSnap = await t.get(eventRef);
// do nothing if event exists
if (eventSnap.exists) {
return null;
}
// add event and update size
await t.update(countRef, { count: i });
return t.set(eventRef, {
completed: admin.firestore.FieldValue.serverTimestamp()
});
}).catch((e: any) => {
console.log(e);
});
// otherwise count all docs in the collection and add size
} else {
const colRef = db.collection(change.after.ref.parent.path);
return db.runTransaction(async (t: any): Promise<any> => {
// update size
const colSnap = await t.get(colRef);
return t.set(countRef, { count: colSnap.size });
}).catch((e: any) => {
console.log(e);
});;
}
}
これは、イベント、増分、およびトランザクションを処理します。これの優れた点は、ドキュメントの正確性が不明な場合(おそらくまだベータ版である場合)、カウンターを削除して、次のトリガーで自動的に追加できるようにすることです。はい、これにはコストがかかるため、それ以外の場合は削除しないでください。
カウントを取得するのと同じ種類:
const collectionPath = 'buildings/138faicnjasjoa89/buildingContacts';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');
また、cronジョブ(スケジュールされた関数)を作成して古いイベントを削除し、データベースストレージのコストを節約することもできます。少なくともブレイズプランが必要であり、さらにいくつかの構成がある可能性があります。たとえば、毎週日曜日の午後11時に実行できます。 https://firebase.google.com/docs/functions/schedule-functions
これはテストされていませんが、いくつかの微調整で動作するはずです。
exports.scheduledFunctionCrontab = functions.pubsub.schedule('5 11 * * *')
.timeZone('America/New_York')
.onRun(async (context) => {
// get yesterday
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const eventFilter = db.collection('_events').where('completed', '<=', yesterday);
const eventFilterSnap = await eventFilter.get();
eventFilterSnap.forEach(async (doc: any) => {
await doc.ref.delete();
});
return null;
});
そして最後に、firestore.rulesのコレクションを保護することを忘れないでください:
match /_counters/{document} {
allow read;
allow write: if false;
}
match /_events/{document} {
allow read, write: if false;
}
更新:クエリ
クエリ数も自動化したい場合は、他の回答に加えて、この変更されたコードをクラウド関数で使用できます。
if (col === 'posts') {
// counter reference - user doc ref
const userRef = after ? after.userDoc : before.userDoc;
// query reference
const postsQuery = db.collection('posts').where('userDoc', "==", userRef);
// add the count - postsCount on userDoc
await addCount(change, context, postsQuery, userRef, 'postsCount');
}
return delEvents();
これにより、userDocumentのpostsCountが自動的に更新されます。この方法で、他の1つを多くのカウントに簡単に追加できます。これは、物事を自動化する方法のアイデアを与えるだけです。イベントを削除する別の方法も紹介しました。それを削除するには、各日付を読み取る必要があります。そのため、後で削除するために実際に保存されるわけではなく、関数が遅くなるだけです。
/**
* Adds a counter to a doc
* @param change - change ref
* @param context - context ref
* @param queryRef - the query ref to count
* @param countRef - the counter document ref
* @param countName - the name of the counter on the counter document
*/
const addCount = async function (change: any, context: any,
queryRef: any, countRef: any, countName: string) {
// events collection
const eventsDoc = '_events';
// simplify event type
const createDoc = change.after.exists && !change.before.exists;
// doc references
const countSnap = await countRef.get();
// increment size if field exists
if (countSnap.get(countName)) {
// createDoc or deleteDoc
const n = createDoc ? 1 : -1;
const i = admin.firestore.FieldValue.increment(n);
// create event for accurate increment
const eventRef = db.doc(`${eventsDoc}/${context.eventId}`);
return db.runTransaction(async (t: any): Promise<any> => {
const eventSnap = await t.get(eventRef);
// do nothing if event exists
if (eventSnap.exists) {
return null;
}
// add event and update size
await t.set(countRef, { [countName]: i }, { merge: true });
return t.set(eventRef, {
completed: admin.firestore.FieldValue.serverTimestamp()
});
}).catch((e: any) => {
console.log(e);
});
// otherwise count all docs in the collection and add size
} else {
return db.runTransaction(async (t: any): Promise<any> => {
// update size
const colSnap = await t.get(queryRef);
return t.set(countRef, { [countName]: colSnap.size }, { merge: true });
}).catch((e: any) => {
console.log(e);
});;
}
}
/**
* Deletes events over a day old
*/
const delEvents = async function () {
// get yesterday
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const eventFilter = db.collection('_events').where('completed', '<=', yesterday);
const eventFilterSnap = await eventFilter.get();
eventFilterSnap.forEach(async (doc: any) => {
await doc.ref.delete();
});
return null;
}
また、ユニバーサル関数はonWrite呼び出し期間ごとに実行されることにも注意してください。特定のコレクションのonCreateおよびonDeleteインスタンスでのみ関数を実行する方が安価な場合があります。私たちが使用しているnoSQLデータベースと同様に、コードとデータを繰り返すことでコストを節約できます。
上記の回答のいくつかに基づいてこれを機能させるのにしばらく時間がかかったので、他の人が使用できるように共有したいと思いました。お役に立てれば幸いです。
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.countDocumentsChange = functions.firestore.document('library/{categoryId}/documents/{documentId}').onWrite((change, context) => {
const categoryId = context.params.categoryId;
const categoryRef = db.collection('library').doc(categoryId)
let FieldValue = require('firebase-admin').firestore.FieldValue;
if (!change.before.exists) {
// new document created : add one to count
categoryRef.update({numberOfDocs: FieldValue.increment(1)});
console.log("%s numberOfDocs incremented by 1", categoryId);
} else if (change.before.exists && change.after.exists) {
// updating existing document : Do nothing
} else if (!change.after.exists) {
// deleting document : subtract one from count
categoryRef.update({numberOfDocs: FieldValue.increment(-1)});
console.log("%s numberOfDocs decremented by 1", categoryId);
}
return 0;
});
私はさまざまなアプローチでいろいろと試しました。そして最後に、私は方法の1つを改善します。最初に、別のコレクションを作成し、そこにすべてのイベントを保存する必要があります。次に、時間によってトリガーされる新しいラムダを作成する必要があります。このラムダは、イベントコレクション内のイベントをカウントし、イベントドキュメントをクリアします。記事のコードの詳細。 https://medium.com/@ihor.malaniuk/how-to-count-documents-in-google-cloud-firestore-b0e65863aeca
このクエリにより、ドキュメントの数がカウントされます。
this.db.collection(doc).get().subscribe((data) => {
count = data.docs.length;
});
console.log(count)
firebaseFirestore.collection("...").addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
int Counter = documentSnapshots.size();
}
});