Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to perform an arrayUnion and an arrayRemove in a single query using Firebase's Firestore?

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?

like image 889
sigmaxf Avatar asked Mar 30 '20 03:03

sigmaxf


People also ask

How do I use Realstore and firestore database?

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.

How do I increment field value in firestore?

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.

What is FieldValue in firebase?

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.


1 Answers

Introduction

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 FieldValue Object

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);

Options

So thus, you are left with three options, ordered by ease-of-use and recommendation:

Option 1: Batched Write

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.

Option 2: Reverse the Array

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));

Option 3: Transaction

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.

Request the Feature

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.

like image 122
samthecodingman Avatar answered Oct 23 '22 12:10

samthecodingman