I am trying to push an array element but am destroying all the content there and replacing with the pushed data:
db .collection('households')
.doc(householdId)
.set( { users: [uid], }, { merge: true }, )
.then(() => { resolve(); })
.catch(() => reject());
I thought the merge true doesn't destroy the data that is already there? Struggling a little with the firestore api docs.
This is the structure of my data:
households
2435djgnfk
users [
0: user1
1: user2
]
Thank you!
You should use Firestore Transaction for this.
const householdRef = db.collection('households').doc(householdId);
const newUid = '1234'; // whatever the uid is...
return db.runTransaction((t) => {
return t.get(householdRef).then((doc) => {
// doc doesn't exist; can't update
if (!doc.exists) return;
// update the users array after getting it from Firestore.
const newUserArray = doc.get('users').push(newUid);
t.set(householdRef, { users: newUserArray }, { merge: true });
});
}).catch(console.log);
Updating an array or a stored object without getting it first will always destroy the older values inside that array/object in firestore.
This is because they are fields and not actual document themselves. So, you have to first get the document and then update the value after that.
I think now you can do it better with the update command on document by using FieldValue.arrayUnion without destroying data that was added meanwhile. Like this:
const admin = require('firebase-admin');
let db = admin.firestore();
const FieldValue = admin.firestore.FieldValue;
let collectionRef = db.collection(collection);
let ref = collectionRef.doc(id);
let setWithOptions = ref.update(arrayFieldName, FieldValue.arrayUnion(value));
As described in https://firebase.googleblog.com/2018/08/better-arrays-in-cloud-firestore.html
Arrays in Firestore don't work like this. According to the documentation:
Although Cloud Firestore can store arrays, it does not support querying array members or updating single array elements.
If you want to change any element in an array, you have to read the array values from the document first, make changes to it in the client, then write the entire array back out.
There are probably other ways to model your data that are better for your use case. That page of documentation linked above has some solutions.
Related
I posted a question earlier about how to query for a specific value inside an array. I was told that there was a similar question already asked. I have looked at the solution for that question however i am getting an error. This is my code currently to check to see if the field mal_id inside the favourites array contains an id passed by me.
const docRef = collection(db, 'users')
const q = query(docRef, where('favourites', 'array-contains', {mal_id: id}))
I then attached an onSnapshot method to see if the data exists which is like this
onSnapshot(q, (snapshot) => {
if(snapshot.data()){
console.log(true)
}
else{
console.log(false)
}
})
But i get an error saying snapshot.data is not a function. idk if you are not supposed to use an onSnapshot method on this kind of query or my code is wrong. I have attached a picture of my database structure. Thanks.
*Edit
So, i have changed my data structure. Now i am creating a new field called animeID and then storing the id into it. I then do the array contains onto that but the snapshot still gives me the same error. Here is the updated code along with a new picture of my database structure.
const docRef = collection(db, 'users')
const q = query(docRef, where('animeID', 'array-contains', `mal_id: ${id}`))
onSnapshot(q, (snapshot) => {
if(snapshot.data()){
console.log(true)
}
else{
console.log(false)
}
})
Updated firestore structure
As Doug commented, Firestore's array-contains operator checks for equivalence between the value you supply and an entire array item. It can't find a subset of an item.
The common workaround is to add an additional field to your document with just the mal_id values you want query on. So something like:
Favourites_mal_ids: [21]
And then you can perform an array-contains on that field.
I need to update every document that has a certain field foo
const firestoreDB = getFirestore(app);
const q = query(collection(firestoreDB, 'myCollection'), where('foo', '!=', false));
But I dont need to read the documents, in order to update their values. But since batch(and document) syntax dictates that I need to have a documentSnapshotRef to update it, this seems like it leads to unnecessary reads.
(async () => {
const docsSnapshot = await getDocs(query);
const batch = writeBatch(firestoreDB);
const docsSnapshot.forEach( (docRef) => {
batch.update(docRef,{
foo: 123, // some number that I need to update
}
});
})()
So in this case, if there are a 100 documents with foo in it, then I would incur 100 reads and 100 writes, while in reality I only needed a 100 writes. Any solution to this? Some way to use query with batch?
As per ZAky's suggestion, I have created a new document called #metadata which contains the document id of each individual document which contains the foo field, as an array.
// fileName: #metadata
fooDocs:[
[0]: 234jhksd32, //document ID auto generated by firestore
//... other document IDs
]
This is specific to firebase, as in order to reference a document, you need to know its document ID. Otherwise you need to query the database, which will create a read on each document that satisfies the condition. You can draw a rough analogy to this with a JS object. To find an entry, either you need to know the key,
const requiredEntry = obj[key] // option 1
or you need to search the whole object to find the object that satisfies a condition(In this case: has the field foo), using a command such as
const requiredEntry = Object.values(obj).find(item=>item.foo) // option 2
So in order to use a method like option 1, in firebase, we read first read the metadata document(incurs 1 read)
(async () => {
const metadataDocSnapshot = await getDoc(doc(firestoreDB, 'myCollection', '#metadata'));
const { fooDocs } = metadataDocSnapshot.data(); // destructure the field that contains our cached document ID's
// to be continued ...
Now we can use these document ID's to directly update the doc, since we can create a documentRef using doc()
// ...
const batch = writeBatch(firestoreDB);
const fooDocs.forEach( (docID) => {
const docRef = doc(firestoreDB, 'myCollection' , docID);
batch.update(docRef,{
foo: 123, // some number that I need to update
}
});
await batch.commit();
})()
P.S. This is my own interpretation of an answer to my own question. Feel free to edit this answer, or post your own, if you think you can solve this better
// Update the other user that current user has asked for revision
export async function updateOtherUserForContractRevision(contractID : string, comments : any) {
// getting userDetails
let currentUser : string | any = localStorage.getItem("userDetails");
currentUser = JSON.parse(currentUser);
// update fields in contractRecieved
const contractsRecievedRef : any = doc(db, "contractsRecieved", currentUser.uid);
// const queryContractsRecieved = query(contractsRecievedRef,
// where('contractDetails.contract.contractID','array-contains',contractID)
// );
// console.log(".////////updateOtherUserForContractRevision", queryContractsRecieved ,contractID)
onSnapshot(contractsRecievedRef, (doc: any) => {
doc.data().contract.map((contract: any) => {
if(contract.contractDetails.contract.contractID === contractID) {
// I reach my desired array Index and want to update the msg field to my comments parametre
console.log(".////////contract", contract);
}
})
})
}
I want to update my msg field as well as content field in contract object, which in turn is present in contractDetails object in the contract array(0th index). I have searched and reached to my desired array value using onSnapShot, how can I update these fields in the onSnapShot method? or I should use another approach for searching and updating fields in objects contained by the array.
JUST A THOUGHT: If I could get the reference to the array index and then use it to update the object, maybe It'll work
for example (I'll update sentForRevision)
onSnapshot(contractsRecievedRef, async (doc: any) => {
doc.data().contract.map(async (contract: any) => {
if(contract.contractDetails.contract.contractID === contractID) {
console.log(".////////contract", doc.ref);
// If this contract.ref exists and points to the index present in the document
await updateDoc(contract.ref,{
"contractDetails.sentForRevision": true,
});
}
})
})
There is no way you can query a Firestore collection based on a value that exists in an object that is contained in an array. This kind of filtering cannot be achieved using partial data. I have even written an article regarding this topic called:
How to update an array of objects in Firestore?
If you need that, you can duplicate the data on which you want to perform the filtering and add it to a separate array. In this way, you can query the collection using array-contains operator. Once you get the desired documents, you can get the array, perform the updates and then write the documents back to Firestore.
I have a list of users in firebase database and I want to make an array of all users whose isDonor value is true. How can I accomplish that?
Something like this should work:
let ref = firebase.database().ref("users");
ref.orderByChild("isDonor").equalTo(true).once("value").then((results) => {
results.forEach((snapshot) => {
console.log(snapshot.key, snapshot.val());
});
});
Also see the Firebase documentation on sorting and filtering data.
Given the data structure below in firebase, i want to run a query to retrieve the blog 'efg'. I don't know the user id at this point.
{Users :
"1234567": {
name: 'Bob',
blogs: {
'abc':{..},
'zyx':{..}
}
},
"7654321": {
name: 'Frank',
blogs: {
'efg':{..},
'hij':{..}
}
}
}
The Firebase API only allows you to filter children one level deep (or with a known path) with its orderByChild and equalTo methods.
So without modifying/expanding your current data structure that just leaves the option to retrieve all data and filter it client-side:
var ref = firebase.database().ref('Users');
ref.once('value', function(snapshot) {
snapshot.forEach(function(userSnapshot) {
var blogs = userSnapshot.val().blogs;
var daBlog = blogs['efg'];
});
});
This is of course highly inefficient and won't scale when you have a non-trivial number of users/blogs.
So the common solution to that is to a so-called index to your tree that maps the key that you are looking for to the path where it resides:
{Blogs:
"abc": "1234567",
"zyx": "1234567",
"efg": "7654321",
"hij": "7654321"
}
Then you can quickly access the blog using:
var ref = firebase.database().ref();
ref.child('Blogs/efg').once('value', function(snapshot) {
var user = snapshot.val();
ref.child('Blogs/'+user+'/blogs').once('value', function(blogSnapshot) {
var daBlog = blogSnapshot.val();
});
});
You might also want to reconsider if you can restructure your data to better fit your use-case and Firebase's limitations. They have some good documentation on structuring your data, but the most important one for people new to NoSQL/hierarchical databases seems to be "avoid building nests".
Also see my answer on Firebase query if child of child contains a value for a good example. I'd also recommend reading about many-to-many relationships in Firebase, and this article on general NoSQL data modeling.
Given your current data structure you can retrieve the User that contains the blog post you are looking for.
const db = firebase.database()
const usersRef = db.ref('users')
const query = usersRef.orderByChild('blogs/efg').limitToLast(1)
query.once('value').then((ss) => {
console.log(ss.val()) //=> { '7654321': { blogs: {...}}}
})
You need to use limitToLast since Objects are sorted last when using orderByChild docs.
It's actually super easy - just use foreslash:
db.ref('Users').child("userid/name")
db.ref('Users').child("userid/blogs")
db.ref('Users').child("userid/blogs/abc")
No need of loops or anything more.