¿Cómo actualizar una "matriz de objetos" con Firestore?

104

Actualmente estoy probando Firestore, y estoy atascado en algo muy simple: "actualizar una matriz (también conocida como un subdocumento)".

Mi estructura de base de datos es super simple. Por ejemplo:

proprietary: "John Doe",
sharedWith:
  [
    {who: "[email protected]", when:timestamp},
    {who: "[email protected]", when:timestamp},
  ],

Estoy tratando (sin éxito) de insertar nuevos registros en una shareWithmatriz de objetos.

He intentado:

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

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

Ninguno funciona. Estas consultas sobrescriben mi matriz.

La respuesta puede ser simple, pero no pude encontrarla ...

nerotulip
fuente

Respuestas:

71

Editar 13/08/2018 : ahora hay soporte para operaciones de arreglos nativos en Cloud Firestore. Vea la respuesta de Doug a continuación.


Actualmente, no hay forma de actualizar un solo elemento de matriz (o agregar / quitar un solo elemento) en Cloud Firestore.

Este código aquí:

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

Esto dice que se establezca el documento de proprietary/docIDtal manera que sharedWith = [{ who: "[email protected]", when: new Date() }no afecte las propiedades del documento existente. Es muy similar a la update()llamada que proporcionó, sin embargo, la set()llamada creará el documento si no existe, mientras que la update()llamada fallará.

Entonces tienes dos opciones para lograr lo que deseas.

Opción 1: configurar toda la matriz

Llame set()con todo el contenido de la matriz, lo que requerirá leer primero los datos actuales de la base de datos. Si le preocupan las actualizaciones simultáneas, puede hacer todo esto en una transacción.

Opción 2: usar una subcolección

Puede hacer sharedWithuna subcolección del documento principal. Luego, agregar un solo elemento se vería así:

firebase.firestore()
  .collection('proprietary')
  .doc(docID)
  .collection('sharedWith')
  .add({ who: "[email protected]", when: new Date() })

Por supuesto, esto viene con nuevas limitaciones. No podrá consultar documentos en función de con quién se comparten, ni podrá obtener el documento y todos lossharedWith datos en una sola operación.

Sam Stern
fuente
9
Esto es muy frustrante ... pero gracias por hacerme saber que no me estoy volviendo loco.
ItJustWerks
50
este es un gran inconveniente, Google tiene que solucionarlo lo antes posible.
Sajith Mantharath
3
La respuesta de @ DougGalante indica que esto se ha solucionado. Usa el arrayUnionmétodo.
quicklikerabbit
152

Firestore ahora tiene dos funciones que le permiten actualizar una matriz sin volver a escribir todo.

Enlace: https://firebase.google.com/docs/firestore/manage-data/add-data , específicamente https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array

Actualizar elementos en una matriz

Si su documento contiene un campo de matriz, puede usar arrayUnion () y arrayRemove () para agregar y eliminar elementos. arrayUnion () agrega elementos a una matriz, pero solo elementos que no están presentes. arrayRemove () elimina todas las instancias de cada elemento dado.

Doug Galante
fuente
56
¿Hay alguna forma de actualizar un índice específico de la matriz?
Artur Carvalho
1
¿Cómo usar esta función de actualización de matriz con "react-native-firebase"? (No puedo encontrar esto en los documentos oficiales de react-native-firebase)
kernelman
4
@ArturCarvalho No, la razón se explica en este video youtube.com/…
Adam
@ArturCarvalho, ¿ha encontrado alguna solución para actualizar un índice específico de la matriz?
Yogendra Patel
3
para quién necesita hacerlo en el cliente, use "import * as firebase from 'firebase / app';" luego "firebase.firestore.FieldValue.arrayUnion (NEW_ELEMENT)"
michelepatrassi
15

Puede usar una transacción ( https://firebase.google.com/docs/firestore/manage-data/transactions ) para obtener la matriz, presionarla y luego actualizar el documento:

    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);
    });
Gabriel McCallin
fuente
1
En la declaración if, debe cambiarlo a esto, porque le falta el documentReferencecomplemento userRefcomo se muestra: transaction.set(userRef, { bookings: [booking] });
Ilir Hushi
8

Aquí está el último ejemplo de la documentación de Firestore:

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")
});
Veeresh Devireddy
fuente
@nifCody, esto de hecho agregará un nuevo elemento de cadena "mayor_virginia" a una matriz "regiones" existente. Lo probé con éxito, y definitivamente no agregué "objeto". Está sincronizado con la pregunta tal como se indica: "impulsar nuevos registros".
Veeresh Devireddy
3

Para aprovechar la respuesta de Sam Stern , también hay una tercera opción que me facilitó las cosas y es usar lo que Google llama un mapa, que es esencialmente un diccionario.

Creo que un diccionario es mucho mejor para el caso de uso que estás describiendo. Normalmente uso matrices para cosas que no se actualizan demasiado, por lo que son más o menos estáticas. Pero para las cosas que se escriben mucho, específicamente los valores que deben actualizarse para los campos que están vinculados a otra cosa en la base de datos, los diccionarios demuestran ser mucho más fáciles de mantener y trabajar.

Entonces, para su caso específico, la estructura de la base de datos se vería así:

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

Esto le permitirá hacer lo siguiente:

var whoEmail = '[email protected]';

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

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

La razón para definir el objeto como una variable es que usarlo 'sharedWith.' + whoEmail + '.when'directamente en el método set resultará en un error, al menos cuando lo use en una función de nube de Node.js.

Horea
fuente
2

Aparte de las respuestas mencionadas anteriormente. Esto lo hará. Usando Angular 5 y AngularFire2. o use firebase.firestore () en lugar de this.afs

  // say you have have the following object and 
  // database structure as you mentioned in your post
  data = { who: "[email protected]", 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
fuente
1

Considere a John Doe como un documento en lugar de una colección

Dale una colección de cosas y cosasSharedWithOthers

Luego, puede mapear y consultar las cosas compartidas de John Doe en esa colección paralela thingsSharedWithOthers.

proprietary: "John Doe"(a document)

things(collection of John's things documents)

thingsSharedWithOthers(collection of John's things being shared with others):
[thingId]:
    {who: "[email protected]", when:timestamp}
    {who: "[email protected]", when:timestamp}

then set thingsSharedWithOthers

firebase.firestore()
.collection('thingsSharedWithOthers')
.set(
{ [thingId]:{ who: "[email protected]", when: new Date() } },
{ merge: true }
)
Richard Taylor-Kenny
fuente
0

Si alguien está buscando una solución sdk de Java Firestore para agregar elementos en el campo de matriz:

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

Para eliminar elementos del usuario de la matriz: FieldValue.arrayRemove()

A_01
fuente
0
addToCart(docId: string, prodId: string): Promise<void> {
    return this.baseAngularFirestore.collection('carts').doc(docId).update({
        products:
        firestore.FieldValue.arrayUnion({
            productId: prodId,
            qty: 1
        }),
    });
}
Blazet
fuente
1
Por favor, explique su respuesta.
Jenea Vranceanu
De hecho, lea stackoverflow.com/help/how-to-answer
jasie hace
-1

Así es como lo hice funcionar. Espero que les ayude a todos, ya que lo veo como una solución mucho mejor. Aquí quiero cambiar el estado de la tarea de abierta (cerrar = falso) a cerrar (cerrar = verdadero), manteniendo todo lo demás igual en el objeto.

closeTask(arrayIndex) {
        let tasks = this.lead.activityTasks;

        const updatedTask = {
          name: tasks[arrayIndex].name,
          start: tasks[arrayIndex].start,
          dueDate:tasks[arrayIndex].dueDate,
          close: true, // This is what I am changing.
        };
        tasks[arrayIndex] = updatedTask;

        const data = {
          activityTasks: tasks
        };

        this.leadService.updateLeadData(data, this.lead.id);
    }

y aquí está el servicio que realmente lo actualiza

 public updateLeadData(updateData, leadId) {
    const leadRef: AngularFirestoreDocument<LeadModel> = this.afs.doc(
      `leads/${leadId}`);

return leadRef.update(updateData);
}
Leandrit Ferizi
fuente
Hola. Tenga en cuenta que este enfoque tiene muchos problemas y riesgos de pérdida / corrupción / inflexibilidad de datos debido a un uso desafortunado de matrices y una "actualización" no incremental. No debe almacenar las tareas en una matriz. En su lugar, téngalos en una colección.
KarolDepka
Lo siento, tuve que votar en contra porque tiene malas prácticas.
KarolDepka