firestore query snapshot reads more than once - javascript

I have a server-side nodejs program which monitors/"listens" to a particular firestore location.
db.collection('temp').where('processed', '==', 'false').onSnapshot(snapshots =>
{
snapshots.forEach(snapshot =>
{
//processes some logic
//db.collection('temp').document(snapshot.id).update('processed': true);
}
}
There are no issues if the query snapshot returns only one snapshot, but if the query snapshot returns more than one snapshot, the logic will be called more times than its intended. For example, if concurrently write n times to the monitored location, the logic is called n^2 times. This will incur costs towards read quotas. How can I make sure that it's only being read once per document?

By looping over snapshot in your onSnapshot() callback you are handling all documents that match the query each time that something changes.
If you only want to handle the documents that were changed, you'll want to loop over snapshot.documentChanges as shown in the Firebase documentation.
If you're always writing documents with processed = false, then you can just handle change.type = added:
db.collection('temp').where('processed', '==', 'false').onSnapshot(snapshots =>
snapshots.docChanges.forEach(function(change) {
if (change.type === "added") {
//processes some logic
//db.collection('temp').document(change.id).update('processed': true);
}

Related

How to update all documents inside Firestore collection if you don't know the id for the document [duplicate]

I have a firestore collections named users, each users have a generated id with a field score :
users
0e8X3VFL56rHBxxgkYOW
score : 4
3SeDjrgAWMmh3ranh2u
score : 5
I use redux-firestore and i want to reset all my users score at 0, something like
firestore.update({ collection: 'users' }, { score : 0 }
I can't achieve this because update method need a document id
Do you know how to do this ?
You can get all the documents in the collection, get their id's and perform updates using those id's:
db.collection("cities").get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
doc.ref.update({
capital: true
});
});
});
For some strange reason the accepted answer ( thehamzarocks ) wasn't working for me, none of the documents were updated. Maybe there's a bug in AngularFire2. Anyway, I decided to loop over the docs array of the QuerySnapshot instead of using its forEach method, and add each update to a batch queue. Batching bulk operations is also more efficient than sending a new update request for each update operation.
resetScore(): Promise<void> {
return this.usersCollectionRef.ref.get().then(resp => {
console.log(resp.docs)
let batch = this.afs.firestore.batch();
resp.docs.forEach(userDocRef => {
batch.update(userDocRef.ref, {'score': 0, 'leadsWithSalesWin': 0, 'leadsReported': 0});
})
batch.commit().catch(err => console.error(err));
}).catch(error => console.error(error))
}
Batch updates are nice but bare in mind that they are limited to 500 document updates per transaction.
If this reset isn't done often maybe simplest approach is:
async function resetScores() {
const collection = await db
.collection("users")
.get()
collection.forEach(doc=> {
doc.ref
.update({
score: 0
})
})
}
I came across this post while searching for similar solutions. Firestore now has batched writes, which will update all documents in one go. This could be an ideal solution for fewer documents.
Updating #thehamzarocks's answer:
const batch = db.batch()
db.collection('cities').get().then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
const docRef = db.collection('cities').doc(doc.id)
batch.update(docRef, { capital: true })
});
batch.commit();
});
Firestore doesn't have the ability to bulk update documents without knowing their IDs. You will have to somehow know the document ID of each document to update (perform a query, or do batches of queries), and update each one individually.
Sorry if the question is old but I thought providing a new answer to this question might be useful to someone else too. I managed to bulk update the entries of a list using the following command:
this.db
.list<User[]>('users')
.set('/', users);
Edit: I'm using AngularFireDatabase.

Best way to batch create if not exists in firestore

I am working with a project where we create a bunch of entries in firestore based on results from an API endpoint we do not control, using a firestore cloud function. The API endpoint returns ids which we use for the document ids, but it does not include any timestamp information. Since we want to include a createdDate in our documents, we are using admin.firestore.Timestamp.now() to set the timestamp of the document.
On subsequent runs of the function, some of the documents will already exist so if we use batch.commit with create, it will fail since some of the documents exist. However, if we use batch.commit with update, we will either not be able to include a timestamp, or the current timestamp will be overwritten. As a final requirement, we do update these documents from a web application and set some properties like a state, so we can't limit the permissions on the documents to disallow update completely.
What would be the best way to achieve this?
I am currently using .create and have removed the batch, but I feel like this is less performant, and I occasionally do get the error Error: 4 DEADLINE_EXCEEDED on the firestore function.
First prize would be a batch that can create or update the documents, but does not edit the createdDate field. I'm also hoping to avoid reading the documents first to save a read, but I'd be happy to add it in if it's the best solution.
Thanks!
Current code is something like this:
const createDocPromise = docRef
.create(newDoc)
.then(() => {
// success, do nothing
})
.catch(err => {
if (
err.details &&
err.details.includes('Document already exists')
) {
// doc already exists, ignore error
} else {
console.error(`Error creating doc`, err);
}
});
This might not be possible with batched writes as set() will overwrite the existing document, update() will update the timestamp and create() will throw an error as you've mentioned. One workaround would be to use create() for each document with Promise.allSettled() that won't run catch() if any of the promise fails.
const results = [] // results from the API
const promises = results.map((r) => db.doc(`col/${r.id}`).create(r));
const newDocs = await Promise.allSettled(promises)
// either "fulfilled" or "rejected"
newDocs.forEach((result) => console.log(result.status))
If any documents exists already, create() will throw an error and status for that should be rejected. This way you won't have to read the document at first place.
Alternatively, you could store all the IDs in a single document or RTDB and filter out duplicates (this should only cost 1 read per invocation) and then add the data.
Since you prefer to keep the batch and you want to avoid reading the documents, a possible solution would be to store the timestamps in a field of type Array. So, you don't overwrite the createdDate field but save all the values corresponding to the different writes.
This way, when you read one of the documents you sort this array and take the oldest value: it is the very first timestamp that was saved and corresponds to the document creation.
This way you don't need any extra writes or extra reads.

Result of querying Firestore Database shows up in console but won't be printed to screen in React Native [duplicate]

I'm learning React and Firestore currently and am a bit stuck. I'm trying to retrieve a users name from a firestore collection by searching their uid.
The following code is executed in a map of 'lessons' to create a list.
{lesson.post_author && findName(lesson.post_author)}
The following code is the findName function.
let findName = uid => {
firebase.firestore().collection("users")
.where('uid', '==', uid)
.get()
.then(querySnapshot => {
console.log(querySnapshot.docs[0].data().name);
});
};
Currently, the findName function will console log all of the names to the console successfully. I've altered the code to be able to console log outside of the firestore call, but that returns a promise pending in console.
The goal of the code is to return the name rather then the uid in the list.
Any help would be much appreciated.
Thank you!
As others have explained, you can't return that value, since it's loaded from Firestore asynchronously. By the time your return runs, the data hasn't loaded yet.
In React you handle this by putting the data in the component's state, and using it from there. If you do this, your render method can simply pick it up from the state, with something like:
{lesson.post_author && findName(lesson.post_author_name)}
(the above assumes that lesson indirectly comes from the state.
It's a bit easier if we pretend there's only one lesson, and you have these values straight in the state:
{state.post_author && findName(state.post_author_name)}
Now I'll assume you already have the post_author and you just need to look up the author's name. That means that somewhere in/after componentDidMount you'll load the additional data and add it to the state:
componentDidMount() {
firebase.firestore().collection("users")
.where('uid', '==', this.state.uid)
.get()
.then(querySnapshot => {
this.setState({ post_user_name: querySnapshot.docs[0].data().name });
});
}
Now the loading of the data still happens asynchronously, so the call to setState() happens some time after componentDidMount has completed. But React is aware that changing the state may require a refresh of the component, so it responds to the call to setState() by rerendering it.
Note that I'd highly recommend using each user's UID as the ID of the documents in users. That way you don't need a query and can just do a directly lookup:
componentDidMount() {
firebase.firestore().collection("users")
.doc(this.state.uid)
.get()
.then(doc => {
this.setState({ post_user_name: doc.data().name });
});
}
I'm trying to retrieve a users name from a firestore collection by
searching their uid.
This is accomplished by using the asyncronous .get method on a Firestore reference. In your case, you probably have a users collection of firebase.auth().currentUser.uid named documents.
var userRef = firebase.firestore().collection('users').doc(users.uid);
userRef.get().then(function(doc) {
if (doc.exists) {
console.log("Users first name is:", doc.data().firstName);
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});

Firebase concurrent read/write

I use Firebase transactions to get compare value and then update the value of a document in a collection.
but when I use the same data and send the query at the same time, they both read the same value so the check I do pass for both, and I have a bad result a the end.
the field is decremented twice.
my transaction is:
let docRef = db.collection('X').doc('SF');
let reduceValue = 20;
let transaction = db.runTransaction(t => {
return t.get(docRef)
.then(doc => {
let value = doc.data().y ;
if (value >= reduceValue) {
t.update(cptRef, { y:FieldValue.increment(-reduceValue) });
return Promise.resolve('Y increased');
} else {
return Promise.reject('sorry y is ');
}
});
}).then(result => {
console.log('Transaction success', result);
}).catch(err => {
console.log('Transaction failure:', err);
});
Thanks.
If I understand correctly your question, what you are describing is the correct behaviour of a Firestore Transaction executed with one of the Firestore Client SDKs:
You are calling twice a function that aims at decrementing a counter;
At the end, the counter is decremented twice.
The fact that your two calls are executed "at the same time" should not change the result: The counter should be decremented twice.
This is exactly what the Transaction ensures: In the case of a concurrent edit, Cloud Firestore runs the entire transaction again, ensuring that the initial doc (db.collection('X').doc('SF') in your case) has not changed during the Transaction. If it is the case, it will retry the operation with the new data.
This is because the Client SDKs use optimistic concurrency for Transactions and that, consequently, there is no locking of the documents.
I suggest you watch the following official video which explains that in detail.
You will also see in this video that for Transactions executed from a back-end (e.g. using the Admin SDK) the mechanism is not the same: they implement pessimistic concurrency (locking documents during transactions).

i want to search for a child in database then remove it but it won't work here

i want to search for a specific user in the ref(likers) then remove it but it won't work here and it alert "likers" when i alert(this.liker) not the user key in the ref(user)
here is the database
and here is the code
firebase.database().ref('posts/-M13xC_yeIsj342A75pz/likers')
.orderByChild('user')
.equalTo('fHI1izTOJ5VeC7ZnjXUducickzj1'/* 'PCrBx38NcjZdsgmRS805sk7lgWn1' firebase.auth().currentUser || {}).uid */)
.on('value', snap => {
this.liker = snap.key
alert(this.liker)
})
firebase.database().ref('posts').child(this.props.postKey/*'-M0IviCqMGE_PxoqNd0W'*/).child('likers').child(key).remove()
When you execute a query against the Firebase Database, there will potentially be multiple results. So the snapshot contains a list of those results. Even if there is only a single result, the snapshot will contain a list of one result.
You code need to handle this fact, by looping over the child nodes of the snapshot you get.
firebase.database().ref('posts/-M13xC_yeIsj342A75pz/likers')
.orderByChild('user')
.equalTo('fHI1izTOJ5VeC7ZnjXUducickzj1'/* 'PCrBx38NcjZdsgmRS805sk7lgWn1' firebase.auth().currentUser || {}).uid */)
.once('value', results => {
results.forEach((snapshot) => {
alert(snapshot.key)
});
})
In the above, I also changed the code to use once instead of on. This won't make any difference to the query results, but means it stops listening after it gets the query results.

Categories

Resources