189

I'm currently trying Firestore, and I'm stuck at something very simple: "updating an array (aka a subdocument)".

My DB structure is super simple. For example:

proprietary: "John Doe",
sharedWith:
  [
    {who: "first@test.com", when:timestamp},
    {who: "another@test.com", when:timestamp},
  ],

I'm trying (without success) to push new records into shareWith array of objects.

I've tried:

// With SET
firebase.firestore()
.collection('proprietary')
.doc(docID)
.set(
  { sharedWith: [{ who: "third@test.com", when: new Date() }] },
  { merge: true }
)

// With UPDATE
firebase.firestore()
.collection('proprietary')
.doc(docID)
.update({ sharedWith: [{ who: "third@test.com", when: new Date() }] })

None works. These queries overwrite my array.

The answer might be simple, but I could'nt find it...

Let Me Tink About It
  • 15,156
  • 21
  • 98
  • 207
charnould
  • 2,480
  • 3
  • 19
  • 23
  • 2
    Hey did you figure it out? I still can't find an answer. – Yash Jain Sep 23 '20 at 21:09
  • For Android, is at simple as [How to update an array of objects in Firestore?](https://medium.com/firebase-tips-tricks/how-to-update-an-array-of-objects-in-firestore-cdb611a56073). – Alex Mamo Aug 11 '21 at 12:03

18 Answers18

221

Firestore now has two functions that allow you to update an array without re-writing the entire thing.

Link: https://firebase.google.com/docs/firestore/manage-data/add-data, specifically https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array

Update elements in an array

If your document contains an array field, you can use arrayUnion() and arrayRemove() to add and remove elements. arrayUnion() adds elements to an array but only elements not already present. arrayRemove() removes all instances of each given element.

Aakash
  • 21,375
  • 7
  • 100
  • 81
Doug Galante
  • 2,401
  • 2
  • 10
  • 6
  • 149
    Is there any way to update a specific index from the array? – Artur Carvalho Aug 31 '18 at 12:50
  • 2
    How to use this array update feature with "react-native-firebase" ? (I cant find this on official docs of react-native-firebase) – kernelman Oct 01 '18 at 11:18
  • 10
    @ArturCarvalho No, the reason why is explained in this video https://www.youtube.com/watch?v=o7d5Zeic63s&feature=youtu.be&list=PLl-K7zZEsYLluG5MCVEzXAQ7ACZBCuZgZ&t=525 – Adam Jan 14 '19 at 03:45
  • @ArturCarvalho have you find any solution for update a specific index from the array? – Yogendra Patel Apr 11 '19 at 12:02
  • 4
    for who needs to do it on the client, use "import * as firebase from 'firebase/app';" then "firebase.firestore.FieldValue.arrayUnion(NEW_ELEMENT)" – michelepatrassi Jan 06 '20 at 18:09
  • 10
    Be great if there was a way to update an item in an array with a specific id. Like an arrayUnion but with a merge: true. At the moment it requires 2 operations to remove the array item and then add it again with the new data. – MadMac Aug 16 '20 at 21:50
117

Edit 08/13/2018: There is now support for native array operations in Cloud Firestore. See Doug's answer below.


There is currently no way to update a single array element (or add/remove a single element) in Cloud Firestore.

This code here:

firebase.firestore()
.collection('proprietary')
.doc(docID)
.set(
  { sharedWith: [{ who: "third@test.com", when: new Date() }] },
  { merge: true }
)

This says to set the document at proprietary/docID such that sharedWith = [{ who: "third@test.com", when: new Date() } but to not affect any existing document properties. It's very similar to the update() call you provided however the set() call with create the document if it does not exist while the update() call will fail.

So you have two options to achieve what you want.

Option 1 - Set the whole array

Call set() with the entire contents of the array, which will require reading the current data from the DB first. If you're concerned about concurrent updates you can do all of this in a transaction.

Option 2 - Use a subcollection

You could make sharedWith a subcollection of the main document. Then adding a single item would look like this:

firebase.firestore()
  .collection('proprietary')
  .doc(docID)
  .collection('sharedWith')
  .add({ who: "third@test.com", when: new Date() })

Of course this comes with new limitations. You would not be able to query documents based on who they are shared with, nor would you be able to get the doc and all of the sharedWith data in a single operation.

Sam Stern
  • 24,624
  • 13
  • 93
  • 124
38

Here is the latest example from the Firestore documentation:

firebase.firestore.FieldValue.ArrayUnion

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")
});
Scoots
  • 3,048
  • 2
  • 21
  • 33
Veeresh Devireddy
  • 1,057
  • 12
  • 24
  • 2
    @nifCody, this will indeed add a new string element "greater_virginia" to an existing array "regions". I have tested it successfully, and definitely not adding "object". It is in sync with the question as stated: "push new records". – Veeresh Devireddy Apr 13 '20 at 00:31
24

You can use a transaction (https://firebase.google.com/docs/firestore/manage-data/transactions) to get the array, push onto it and then update the document:

    const booking = { some: "data" };
    const userRef = this.db.collection("users").doc(userId);

    this.db.runTransaction(transaction => {
        // This code may get re-run multiple times if there are conflicts.
        return transaction.get(userRef).then(doc => {
            if (!doc.data().bookings) {
                transaction.set({
                    bookings: [booking]
                });
            } else {
                const bookings = doc.data().bookings;
                bookings.push(booking);
                transaction.update(userRef, { bookings: bookings });
            }
        });
    }).then(function () {
        console.log("Transaction successfully committed!");
    }).catch(function (error) {
        console.log("Transaction failed: ", error);
    });
10

Sorry Late to party but Firestore solved it way back in aug 2018 so If you still looking for that here it is all issues solved with regards to arrays.

https://firebase.googleblog.com/2018/08/better-arrays-in-cloud-firestore.htmlOfficial blog post

array-contains, arrayRemove, arrayUnion for checking, removing and updating arrays. Hope it helps.

Guru
  • 286
  • 3
  • 6
6

To build on Sam Stern's answer, there is also a 3rd option which made things easier for me and that is using what Google call a Map, which is essentially a dictionary.

I think a dictionary is far better for the use case you're describing. I usually use arrays for stuff that isn't really updated too much, so they are more or less static. But for stuff that gets written a lot, specifically values that need to be updated for fields that are linked to something else in the database, dictionaries prove to be much easier to maintain and work with.

So for your specific case, the DB structure would look like this:

proprietary: "John Doe"
sharedWith:{
  whoEmail1: {when: timestamp},
  whoEmail2: {when: timestamp}
}

This will allow you to do the following:

var whoEmail = 'first@test.com';

var sharedObject = {};
sharedObject['sharedWith.' + whoEmail + '.when'] = new Date();
sharedObject['merge'] = true;

firebase.firestore()
.collection('proprietary')
.doc(docID)
.update(sharedObject);

The reason for defining the object as a variable is that using 'sharedWith.' + whoEmail + '.when' directly in the set method will result in an error, at least when using it in a Node.js cloud function.

Horea
  • 201
  • 2
  • 14
5

#Edit (add explanation :) ) say you have an array you want to update your existing firestore document field with. You can use set(yourData, {merge: true} ) passing setOptions(second param in set function) with {merge: true} is must in order to merge the changes instead of overwriting. here is what the official documentation says about it

An options object that configures the behavior of set() calls in DocumentReference, WriteBatch, and Transaction. These calls can be configured to perform granular merges instead of overwriting the target documents in their entirety by providing a SetOptions with merge: true.

you can use this

const yourNewArray = [{who: "first@test.com", when:timestamp}
{who: "another@test.com", when:timestamp}]    


collectionRef.doc(docId).set(
  {
    proprietary: "jhon",
    sharedWith: firebase.firestore.FieldValue.arrayUnion(...yourNewArray),
  },
  { merge: true },
);

hope this helps :)

Benyam
  • 329
  • 4
  • 6
  • 1
    While this might answer the question, if possible you should [edit] your answer to include a short explanation of *how* this code block answers the question. This helps to provide context and makes your answer much more useful to future readers. – Hoppeduppeanut Nov 30 '20 at 00:04
  • 1
    @Hoppeduppeanut yes you are right. I definitely accept the criticism for not adding an explanation for my answer. I have edited my answer. hope it helps much better now. – Benyam Dec 01 '20 at 12:37
3
addToCart(docId: string, prodId: string): Promise<void> {
    return this.baseAngularFirestore.collection('carts').doc(docId).update({
        products:
        firestore.FieldValue.arrayUnion({
            productId: prodId,
            qty: 1
        }),
    });
}
iknow
  • 8,358
  • 12
  • 41
  • 68
Blazet
  • 31
  • 1
3

We can use arrayUnion({}) method to achive this.

Try this:

collectionRef.doc(ID).update({
    sharedWith: admin.firestore.FieldValue.arrayUnion({
       who: "first@test.com",
       when: new Date()
    })
});

Documentation can find here: https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array

Ind
  • 41
  • 5
3

i know this is really old, but to help people newbies with the issue

firebase V9 provides a solution using the arrayUnion and arrayRemove

await updateDoc(documentRef, {
    proprietary: arrayUnion( { sharedWith: [{ who: "third@test.com", when: new Date() }] }
});

check this out for more explanation

Preye
  • 39
  • 1
  • 5
2

Other than the answers mentioned above. This will do it. Using Angular 5 and AngularFire2. or use firebase.firestore() instead of this.afs

  // say you have have the following object and 
  // database structure as you mentioned in your post
  data = { who: "third@test.com", when: new Date() };

  ...othercode


  addSharedWith(data) {

    const postDocRef = this.afs.collection('posts').doc('docID');

    postDocRef.subscribe( post => {

      // Grab the existing sharedWith Array
      // If post.sharedWith doesn`t exsit initiated with empty array
      const foo = { 'sharedWith' : post.sharedWith || []};

      // Grab the existing sharedWith Array
      foo['sharedWith'].push(data);

      // pass updated to fireStore
      postsDocRef.update(foo);
      // using .set() will overwrite everything
      // .update will only update existing values, 
      // so we initiated sharedWith with empty array
    });
 }  
Jassi
  • 619
  • 7
  • 12
1

Consider John Doe a document rather than a collection

Give it a collection of things and thingsSharedWithOthers

Then you can map and query John Doe's shared things in that parallel thingsSharedWithOthers collection.

proprietary: "John Doe"(a document)

things(collection of John's things documents)

thingsSharedWithOthers(collection of John's things being shared with others):
[thingId]:
    {who: "first@test.com", when:timestamp}
    {who: "another@test.com", when:timestamp}

then set thingsSharedWithOthers

firebase.firestore()
.collection('thingsSharedWithOthers')
.set(
{ [thingId]:{ who: "third@test.com", when: new Date() } },
{ merge: true }
)
1

If You want to Update an array in a firebase document. You can do this.

    var documentRef = db.collection("Your collection name").doc("Your doc name")

    documentRef.update({
yourArrayName: firebase.firestore.FieldValue.arrayUnion("The Value you want to enter")});
1

Although firebase.firestore.FieldValue.arrayUnion() provides the solution for array update in firestore, at the same time it is required to use {merge:true}. If you do not use {merge:true} it will delete all other fields in the document while updating with the new value. Here is the working code for updating array without loosing data in the reference document with .set() method:


const docRef = firebase.firestore().collection("your_collection_name").doc("your_doc_id");

docRef.set({yourArrayField: firebase.firestore.FieldValue.arrayUnion("value_to_add")}, {merge:true});

TradeCoder
  • 1,832
  • 3
  • 7
  • 18
1

If the document contains a nested object in the form of an array, .dot notation can be used to reference and update nested fields. Node.js example:

const users = {
  name: 'Tom',
  surname: 'Smith',
  favorites: {
    sport: 'tennis',
    color: 'red',
    subject: 'math'
  }
};

const update = await db.collection('users').doc('Tom').update({
  'favorites.sport': 'snowboard'
});

or Android sdk example:

db.collection("users").document("Tom")
        .update(
               'favorites.sport': 'snowboard'
        );
MGLabs
  • 91
  • 6
1
db.collection("collection")
 .doc("docId")
 .update({arrayOfObj: fieldValue.arrayUnion({...item})})
Otpidus
  • 489
  • 5
  • 5
0

If anybody is looking for Java firestore sdk solution to add items in array field:

List<String> list = java.util.Arrays.asList("A", "B");
Object[] fieldsToUpdate = list.toArray();
DocumentReference docRef = getCollection().document("docId");
docRef.update(fieldName, FieldValue.arrayUnion(fieldsToUpdate));

To delete items from array user: FieldValue.arrayRemove()

A_01
  • 1,021
  • 11
  • 27
0

There is a simple hack in firestore:

use path with "." as property name:

propertyname.arraysubname.${id}: