Чи можна порахувати, скільки предметів має колекція, використовуючи нову базу даних Firebase Cloud Cloudstore?
Якщо так, то як це зробити?
Чи можна порахувати, скільки предметів має колекція, використовуючи нову базу даних Firebase Cloud Cloudstore?
Якщо так, то як це зробити?
Відповіді:
Як і в багатьох питаннях, відповідь така - Це залежить .
Ви повинні бути дуже обережними, обробляючи велику кількість даних на передній частині. Крім того, щоб передній вигляд відчував себе млявим, Firestore також стягує з вас 0,60 долара за мільйон прочитаних вами читань .
Будьте обережні - користувальницький досвід Frontend може стати хітом
Поводження з цим на передньому кінці має бути прекрасним, доки ви не будете робити занадто багато логіки з цим повернутим масивом.
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 року Firestore дозволяє нарощувати лічильники, повністю атомно, без попереднього зчитування даних . Це гарантує, що ми маємо правильні значення лічильників навіть при оновленні з декількох джерел одночасно (раніше вирішених за допомогою транзакцій), а також зменшуємо кількість виконаних нами читання баз даних.
Прослуховуючи будь-який документ, який видаляє або створює, ми можемо додавати або видаляти з поля рахунку, що знаходиться в базі даних.
Подивіться документи «firestore» - розповсюджені лічильники або ознайомтеся з агрегацією даних Джеффа Делані. Його путівники по-справжньому фантастичні для тих, хто використовує 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;
});
Тепер на фронті ви можете просто запитати це полеOfDocs для отримання розміру колекції.
firestore.runTransaction { ... }
блок. Це виправляє проблеми з одночасністю доступу numberOfDocs
.
Найпростіший спосіб зробити це - прочитати розмір "querySnapshot".
db.collection("cities").get().then(function(querySnapshot) {
console.log(querySnapshot.size);
});
Ви також можете прочитати довжину масиву документів всередині "querySnapshot".
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: 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 в об'єкті контексту. Якщо функцію обробляли багато разів для однієї події, ідентифікатор події буде однаковим у всіх випадках. На жаль, у вашій базі даних має бути колекція "подій".
context.eventId
завжди буде однаково на кількох викликах одного і того ж тригера? У моєму тестуванні це здається послідовним, але я не можу знайти жодної "офіційної" документації, яка б це пояснила.
У 2020 році це все ще не доступне в Firebase SDK, однак воно доступне в розширеннях Firebase (бета-версія), проте налаштування та використання його досить складне ...
Розумний підхід
Допоміжники ... (створення / видалення здається зайвим, але дешевше, ніж 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, якщо ви виконаєте такий запит , це коштуватиме дорожче .
[Порада для розробників перед початком їхніх проектів]
Оскільки ми передбачили цю ситуацію на початку, ми можемо фактично скласти колекцію, а саме лічильники з документом для зберігання всіх лічильників у полі з типом 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()
. Нижче наведено два способи, за допомогою яких можна знайти кількість документів у колекції.
db.collection("CollectionName").get().subscribe(doc=>{
console.log(doc.size)
})
Використовуючи вищезгаданий код, ваш документ, що читається, буде дорівнює розміру документів у колекції, і саме тому потрібно уникати використання вищезгаданого рішення.
db.collection("CollectionName").doc("counts")get().subscribe(doc=>{
console.log(doc.count)
})
Вище ми створили документ з лічильниками імен, щоб зберігати всю інформацію про підрахунок. Ви можете оновити документ обліку таким чином: -
ціна wrt (читання документа = 1) та швидке пошуку даних вищезазначене рішення є хорошим.
Збільшення лічильника за допомогою адмін.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
поновлення означає , що ви витрачаєте більше грошей на непотрібні функції викликів (якщо ви оновлюєте документи в колекції).
Вирішення завдання полягає в тому, щоб:
запишіть лічильник у файл firebase doc, який ви збільшуєте в межах транзакції щоразу, коли створюєте новий запис
Ви зберігаєте підрахунок у полі вашого нового запису (тобто: позиція: 4).
Потім ви створюєте індекс у цьому полі (позиція DESC).
Ви можете зробити пропуск + обмеження за допомогою запиту.Where ("позиція", "<" x) .OrderBy ("позиція", DESC)
Сподіваюся, це допомагає!
Я створив універсальну функцію, використовуючи всі ці ідеї для обробки всіх зустрічних ситуацій (крім запитів).
Єдиним винятком буде, коли робиш так багато записує секунду, це сповільнює тебе. Прикладом може бути подобається на модній посаді. Наприклад, це зайва нагода в блозі і обійдеться вам дорожче. Я пропоную створити в цьому випадку окрему функцію за допомогою осколків: 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:00. 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();
Який автоматично оновлює записиCount у користувальницькому Документі. Таким чином, ви можете легко додати ще одне до багатьох підрахунків. Це просто дає вам уявлення про те, як ви можете автоматизувати речі. Я також дав вам інший спосіб видалити події. Ви повинні прочитати кожну дату, щоб видалити її, тому вона не дійсно заощадить вас, щоб видалити їх пізніше, просто робить функцію повільніше.
/**
* 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;
});
Я багато пробував з різними підходами. І нарешті, я вдосконалюю один із методів. Спочатку потрібно створити окрему колекцію і зберегти там усі події. По-друге, вам потрібно створити нову лямбда, яку слід викликати часом. Ця лямбда буде рахувати події під час збору подій та очищати документи про події. Деталі коду в статті. 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)
При цьому використовується підрахунок для створення цифрового унікального ідентифікатора. У моєму використанні я не буду декрементувати ніколи , навіть коли document
видалено те, для чого потрібен ідентифікатор.
Після collection
створення, яке потребує унікального числового значення
appData
з одним документом, set
з .doc
ідентифікаторомonly
uniqueNumericIDAmount
0 уfirebase firestore console
doc.data().uniqueNumericIDAmount + 1
як унікальний числовий ідентифікаторappData
колекцію за uniqueNumericIDAmount
допомогоюfirebase.firestore.FieldValue.increment(1)
firebase
.firestore()
.collection("appData")
.doc("only")
.get()
.then(doc => {
var foo = doc.data();
foo.id = doc.id;
// your collection that needs a unique ID
firebase
.firestore()
.collection("uniqueNumericIDs")
.doc(user.uid)// user id in my case
.set({// I use this in login, so this document doesn't
// exist yet, otherwise use update instead of set
phone: this.state.phone,// whatever else you need
uniqueNumericID: foo.uniqueNumericIDAmount + 1
})
.then(() => {
// upon success of new ID, increment uniqueNumericIDAmount
firebase
.firestore()
.collection("appData")
.doc("only")
.update({
uniqueNumericIDAmount: firebase.firestore.FieldValue.increment(
1
)
})
.catch(err => {
console.log(err);
});
})
.catch(err => {
console.log(err);
});
});
firebaseFirestore.collection("...").addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
int Counter = documentSnapshots.size();
}
});