I'm using firestore to update an array on my object. I found on the documentation that I can perform array unions and removes, which is great, here is an example given on the documentation:
var washingtonRef = db.collection("cities").doc("DC");
// Atomically add a new region to the "regions" array field.
washingtonRef.update({
regions: firebase.firestore.FieldValue.arrayUnion("greater_virginia")
});
// Atomically remove a region from the "regions" array field.
washingtonRef.update({
regions: firebase.firestore.FieldValue.arrayRemove("east_coast")
});
As you can see, there are 2 separate queries to the database, which makes my application twice as expensive in terms of database calls, so I want to group them in a single query, something like this:
var washingtonRef = db.collection("cities").doc("DC");
washingtonRef.update({
regions: firebase.firestore.FieldValue.arrayUnion("greater_virginia"),
regions: firebase.firestore.FieldValue.arrayRemove("east_coast")
});
Unfortunately, this doesn't work, and only the last command get executed.
Is there a way to get this to work?
The Firestore and the Firebase Realtime database are NoSQL database that means there is no need to create the table and define the schema. In the Realtime database, data is stored as one large JSON tree. In Cloud Firestore, data is stored as a collection of documents. The data is simple, so it is very easy to store.
Firestore now has a specific operator for this called FieldValue. increment() . By applying this operator to a field, the value of that field can be incremented (or decremented) as a single operation on the server.
elements : any [] ) : FieldValue. Returns a special value that can be used with set() or update() that tells the server to union the given elements with any array value that already exists on the server. Each specified element that doesn't already exist in the array will be added to the end.
Edit: This answer was originally written before the modern modular SDK was released, it has been updated to cover this new SDK as well as the legacy namespaced SDK available at the time. For new projects, use the modular SDK.
The reason your combined instruction doesn't work (aside from the syntax error) is because of the way the FieldValue object is defined.
Let's say you defined the following objects ready to use in your update()
call:
// Firebase Namespaced SDK (v8 & older)
// import firebase as appropriate
const myArrayUnion = firebase.firestore.FieldValue.arrayUnion("greater_virginia")
const myArrayRemove = firebase.firestore.FieldValue.arrayRemove("east_coast")
// Firebase Modular SDK (v9+)
import { arrayUnion, arrayRemove } from "firebase/firestore";
const myArrayUnion = arrayUnion("greater_virginia")
const myArrayRemove = arrayRemove("east_coast")
The objects returned are implementations of a FieldValue
class and are the equivalent of
const myArrayUnion = {
_method: "FieldValue.arrayUnion",
_elements: ["greater_virginia"]
}
const myArrayRemove = {
_method: "FieldValue.arrayRemove",
_elements: ["east_coast"]
}
Then, based on the value of _method
, the appropriate field transformation instruction is serialised using this code and sent off to the Firestore API. Because the operation is switched based on the value of _method
, only one of either arrayUnion
or arrayRemove
can take place on a single instruction.
arrayUnion
and arrayRemove
Both arrayUnion
and arrayRemove
can take multiple arguments, adding each to the internal _elements
array shown above.
Therefore to add both "value1"
and "value2"
to the specified field at the same time, you would use:
// Firebase Namespaced SDK (v8 & older)
firebase.firestore.FieldValue.arrayUnion("value1", "value2");
// Firebase Modular SDK (v9+)
arrayUnion("value1", "value2");
To add an array of items to the specified field at the same time, you would use:
// Firebase Namespaced SDK (v8 & older)
const addedElements = ["greater_virginia", "east_coast", "central"];
firebase.firestore.FieldValue.arrayUnion.apply(null, addedElements);
// or
firebase.firestore.FieldValue.arrayUnion(...addedElements);
// Firebase Modular SDK (v9+)
const addedElements = ["greater_virginia", "east_coast", "central"];
arrayUnion.apply(null, addedElements);
// or
arrayUnion(...addedElements);
So thus, you are left with three options, ordered by ease-of-use and recommendation:
Using a batched write will allow you to write the instructions to the database together and if either part fails, nothing will be changed.
// Firebase Namespaced SDK (v8 & older)
// import firebase as appropriate
var db = firebase.firestore();
var washingtonRef = db.collection("cities").doc("DC");
var batch = db.batch();
batch.update(washingtonRef, {regions: firebase.firestore.FieldValue.arrayUnion("greater_virginia")});
batch.update(washingtonRef, {regions: firebase.firestore.FieldValue.arrayRemove("east_coast")});
batch.commit()
.then(() => console.log('Success!'))
.catch(err => console.error('Failed!', err));
// Firebase Modular SDK (v9+)
import { getFirestore, arrayRemove, arrayUnion, doc, writeBatch } from "firebase/firestore";
const db = getFirestore();
const washingtonRef = doc(db, "cities", "DC");
const batch = writeBatch(db);
batch.update(washingtonRef, {regions: arrayUnion("greater_virginia")});
batch.update(washingtonRef, {regions: arrayRemove("east_coast")});
batch.commit()
.then(() => console.log('Success!'))
.catch(err => console.error('Failed!', err));
Note: Each batched write can contain up to 500 writes.
At the time this answer was originally written (having since been removed), each transformation instruction (arrayUnion
, arrayRemove
, increment
and serverTimestamp
) would count as 2 operations to this limit because both reads and writes were counted meaning only 250 transforms could be used in a batch.
This particular data structure is reminiscent of the Realtime Database and before the arrayUnion
and arrayRemove
operations were introduced.
The general premise is that an array is transformed prior to uploading it to the database.
const originalArr = ["greater_virginia", "east_coast", "central"]
is reversed and stored as
const keyedObjectOfArr = {
"greater_virginia": 1
"east_coast": 1,
"central": 1
}
The above result can be achieved using:
const keyedObjectOfArr = originalArr.reduce((acc, v) => (acc[v] = 1, acc), {});
And returned to normal using
const originalArr = Object.keys(keyedObjectOfArr);
Then when you wanted to apply unions/removes you would use the following:
// Firebase Namespaced SDK (v8 & older)
// import firebase as appropriate
/**
* Creates (or adds to the given object) changes to be committed to the database.
*
* Note: Add operations will override remove operations if they exist in both arrays.
*
* @param fieldPath The path to the 'array' field to modify
* @param addedArray (optional) Elements to be added to the field
* @param removedArray (optional) Elements to be removed from the field
* @param changes (optional) A previous changes object for chaining
*/
function addArrayChanges(fieldPath, addedArray = [], removedArray = [], changes = {}) {
var fvDelete = firebase.firestore.FieldValue.delete();
removedElements.forEach(e => changes[fieldPath + '.' + e] = fvDelete);
addedElements.forEach(e => changes[fieldPath + '.' + e] = 1);
return changes;
}
var washingtonRef = db.collection("cities").doc("DC");
var addedElements = ["greater_virginia"];
var removedElements = ["east_coast"];
var changes = addArrayChanges("regions", addedElements, removedElements);
washingtonRef.update(changes)
.then(() => console.log('Success!'))
.catch(err => console.error('Failed!', err));
// Firebase Modular SDK (v9+)
import { getFirestore, arrayRemove, arrayUnion, deleteField, doc, updateDoc, writeBatch } from "firebase/firestore";
/**
* Creates (or adds to the given object) changes to be committed to the database.
*
* Note: Add operations will override remove operations if they exist in both arrays.
*
* @param fieldPath The path to the 'array' field to modify
* @param addedArray (optional) Elements to be added to the field
* @param removedArray (optional) Elements to be removed from the field
* @param changes (optional) A previous changes object for chaining
*/
function addArrayChanges(fieldPath, addedArray = [], removedArray = [], changes = {}) {
const fvDelete = deleteField();
removedElements.forEach(e => changes[fieldPath + '.' + e] = fvDelete);
addedElements.forEach(e => changes[fieldPath + '.' + e] = 1);
return changes;
}
const db = getFirestore();
const washingtonRef = doc(db, "cities", "DC");
const addedElements = ["greater_virginia"];
const removedElements = ["east_coast"];
const changes = addArrayChanges("regions", addedElements, removedElements);
updateDoc(washingtonRef, changes)
.then(() => console.log('Success!'))
.catch(err => console.error('Failed!', err));
While this method is an option, it's impractical for this use case and advised against. This blog post covers arrays in the Firebase Realtime Database, but these concerns also apply when concerned with the contents of a single document at scale.
There is probably room within the Firestore API to support both adding and removing array entries at the same time due to them being separated at the serialization layer.
interface FieldTransform {
fieldPath?: string;
setToServerValue?: FieldTransformSetToServerValue;
appendMissingElements?: ArrayValue;
removeAllFromArray?: ArrayValue;
increment?: Value;
}
So you may also submit a Feature Request and see what happens.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With