Is it possible to update firestore using the previous state?
So for example I have an address document which has a users field which holds an array of users associated with the address.
whenever I want to add a new user to this array I need the previous array otherwise I will end up overwriting the current data with the new data.
So I end up with something like.
firestore()
.collection("addresses")
.doc(addressId)
.get()
.then(doc => {
this.db
.collection("addresses")
.doc(addressId)
.update({
users: [...doc.data().users, id]
})
});
Is there a way to access the previous data without having to nest calls?
if not
Is there a better way to manage relationships?
If you need the previous value to determine the new value, you should use a transaction. This is the only way to ensure that different clients aren't accidentally overwriting each other's actions.
Unfortunately transactions also need nested calls, since that is the only way to get the current value, and even have one extra wrapper (for the transaction.
var docRef = firestore()
.collection("addresses")
.doc(addressId);
return db.runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(docRef).then(function(doc) {
transaction.update(docRef, { users: [...doc.data().users, id ]});
});
}).then(function() {
console.log("Transaction successfully committed!");
}).catch(function(error) {
console.log("Transaction failed: ", error);
});
The optimal solution is to use a data structure that doesn't require the current value to add new values. This is one of the reasons Firebase recommends against using arrays: they're inherently hard to scale when multiple users may be adding items to the array. If there is no need for maintaining order between the users, I'd recommend using a set-like structure for the users:
users: {
id1: true,
id2: true
}
This is a collection with two users (id1 and id2). The true values are just markers, since you can't have a field without a value.
With this structure, adding a user is as easy as:
firestore()
.collection("addresses")
.doc(addressId)
.update({ "users.id3": true })
Also see the Firestore documentation on
Working with Arrays, Lists, and Sets
Related
Enviroment: nodejs, firebase-admin, firestore.
Database scructure (space):
Database scructure (user):
Creating new space (example):
// init data
const userId = "someUserId";
// Create new space
const spaceRef = await db.collection("spaces").add({ name: "SomeName" });
// Get spaceId
spaceId = spaceRef.id;
// Get user Doc for upate their spaces
const userRef = await db.collection("users").doc(userId);
// Add "spaceId" to user spaces list
userRef.collection("spaces").doc(spaceId).set({ some: "data" });
// Create collection "members" in new space with "userId"
spaceRef.collection("members").doc(userId).set({role: "OWNER"})
Question: I want to execute this code inside single runTransaction, but as I see transactions support only one-time read and multiple update, this does not suit me, since I get the spaceId I need during the execution of the code.
Why I want to use transaction: In my data structure, the relationship between the created space and the presence of the ID of this space on the user is required. If we assume that an error occurred during the execution of this code, for example, the space was created, but this space was not added inside the user profile, then this will be a fatal problem in my database structure.
Similar to other databases, transactions solve this problem, but I can't figure out how to do it with firestore.
Maybe you know a better way to protect yourself from consistent data in this case?
Actually, you don't need a Transaction for that, since you are not reading documents.
With db.collection("users").doc(userId); you are actually not reading a document, just calling "locally" the doc() method to create a DocumentReference. This method is not asynchronous, therefore you don't need to use await. To read the doc, you would use the asynchronous get() method.
So, using a batched write, which atomically commits all pending write operations to the database, will do the trick:
const userId = 'someUserId';
const userRef = db.collection('users').doc(userId);
const spaceRef = firestore.collection('spaces').doc();
const spaceId = spaceRef.id;
const writeBatch = firestore.batch();
writeBatch.set(spaceRef, { name: "SomeName" });
writeBatch.set(userRef.collection("spaces").doc(spaceId), { some: "data" });
writeBatch.set(spaceRef.collection("members").doc(userId), {role: "OWNER"});
await writeBatch.commit();
You should include this code in a try/catch block and if the batch commit fails you will be able to handle this situation in the catch block, knowing that none of the write were committed.
The goal is to update a Firebase document.
The document ID is unknown at the time of updating. What is known is a property that acts as unique key on the document (email address in the case below).
Based on the official documentation and this answer, the code below works as a method for updating an array within the document.
This feels clunky, though.
Are there more direct methods for retrieving a reference to a single document and updating its fields?
// Set query.
let query = firebase.firestore().collection('users').where('emailAddress', '==', 'test#test.com');
// Run query.
try {
const querySnapshot = await query.get();
return querySnapshot.docs[0].ref.update({
designs: firebase.firestore.FieldValue.arrayUnion('foobar')
})
} catch(e) {
console.log('Error getting user: ', e);
}
No, what you're doing is the best you can do. If you don't know the ID, you have to query to find it. Firestore has no equivalent of a SQL "update where" command.
I have a collection in a firebase. I'm listening to changes with onSnapshot method.
If any item from a collection is changed in any way listener is fired. I am getting new data, which was inserted into my collection with docChanges method
db.collection('collection')
.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
const payload = {
id: change.doc.id,
data: change.doc.data(),
};
...... some action
});
});
and now... I need to compare new data (which was just inserted) with old data (from before insert) and I am wondering if there is any way to do that?
The Firestore SDK doesn't give you any ways to detect the differences between the "before" and "after" states of a document's fields. You will have to remember both snapshots and look for differences in the fields yourself. You might want to do a search for some ideas on how to go about this, for example: Generic deep diff between two objects
I'm building a chat app. When a user makes an update on their local profile I'd like to use cloud functions to make that update across a collectionGroup.
I'm successfully listening to the update in cloud functions and retrieving a list of collectionGroups with the following:
const collectionGroupNameref = await db.collectionGroup('collectionGroupName').where('userId', '==', data.uid).get();
collectionGroupNameref.forEach(async (val: any) => {
const connectionsRef = await db.collection('collectionGroupName').doc(val.id).get();
});
But now I need to update a field within that collectionGroup and that's where I'm running into issues.
The collectionGroup is stored in 2 locations:
users{id}collectionGroupName{id}
groups{id}collectionGroupName{id}
Is it possible to update all of the documents in that collectionGroup
Firestore doesn't provide any methods to update an entire collection or collection group like "UPDATE WHERE" in SQL. What you are required to do instead is write each document individually. So, if you've already executed a query for documents in the collection group, can you simply iterate the documents in the result set and update each document as needed. You can use the ref property of DocumentSnapshot to easily update each document, no matter what collection contains it.
const querySnapshot = await db
.collectionGroup('collectionGroupName')
.where('userId', '==', 'data.uid')
.get();
querySnapshot.docs.forEach(snapshot => {
snapshot.ref.update(...)
})
I have an application where a user can create lists.
The user can share a list with other users. I already manage to create the sharing part, the one I'm having issues is with the delete part. I want that when a user deletes a list that is shared , this list is also deleted from the other users.
This delete will be made only by list owner.
So a scenario will be:
User A creates a List with pushID = 1.
This list is added in the following firebase ref: /userList/$userAID/$pushID.
User A shares list with User B and User C.
This list is added in the following firebase ref: /userList/$userBID/$pushID and /userList/$userCID/$pushID.
User A deletes list with pushID = 1.
So in my
So i have this schema:
userList: {
2xYnKcZFEdPYWfUJ3E63yQEDShe2: {
-Kt7lXiY0Yt-oDcV38L5
}
KtQHkXMSwKSByZ1rmTRwjDmSYnE3: {
-Kt7lXiY0Yt-oDcV38L5: {}
-Kt9XP91hjwcwgcBSgbc: {}
}
XHpMVoRqcCdzwTP70L29Lza1ibD3: {
-Kt7lXiY0Yt-oDcV38L5: {}
}
}
In high level this will be:
userList: {
userID: (A) {
-listID : (1) {}
}
userID: (B) {
-listID: (1) {}
-listID: (2) {}
}
userID: (C) {
-listID: (1) {}
-listID: (3) {}
-listID: (4) {}
}
}
The current code I have to do this is the following:
const ref = firebase.database().ref('userList');
ref.once('value')
.then((snapshot) => {
snapshot.forEach((childSnapshot) => {
ref.child(childSnapshot.key)
.once('value')
.then((snapshot2) => {
snapshot2.forEach((childSnapshot2) => {
if (childSnapshot2.key === uid) {
ref.child(childSnapshot2.key)
.remove();
}
});
})
.catch(() => {
console.log('error2');
});
});
})
.catch((error) => {
console.log(error);
});
What I'm doing in this code is, first fetching ALL the list inside userList, by getting the key I manage to jump to the userID child. Inside this node once again I manage to jump inside the pushID where I make a validation of checking if current key is equal to the UID of the list i want to delete, if so I do a remove().
I feel there must be a better way of telling Firebase to go directly to pushID and find all of those that are equal to the UID of the list I want to delete and then do it.
There is no way to do a server-side delete of multiple items based on a condition with the Firebase Database. You must first retrieve the items (or just their IDs) matching the condition and then delete those.
However, you can delete the list itself and all references to it in one go by using a multi-path update.
I'd also recommend keeping a list of all the UIDs you've shared a specific list with, so that you don't have to loop over all users. Keeping many-to-many relations in both directions is quite common in Firebase and other NoSQL databases.
For more see:
the blog post introducing multi-path updates
the blog post describing client-side fan-out using multi-path updates
my answer on strategies for updating denormalized data
my answer on modeling many-to-many relationships