Comment mettre à jour un «tableau d'objets» avec Firestore?

104

J'essaye actuellement Firestore, et je suis coincé à quelque chose de très simple: "mettre à jour un tableau (aka un sous-document)".

Ma structure DB est super simple. Par exemple:

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

J'essaye (sans succès) de pousser de nouveaux enregistrements dans un shareWithtableau d'objets.

J'ai essayé:

// 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() }] })

Aucun ne fonctionne. Ces requêtes écrasent mon tableau.

La réponse est peut-être simple, mais je n'ai pas pu la trouver ...

nérotulipe
la source

Réponses:

71

Edit 13/08/2018 : les opérations de baies natives sont désormais prises en charge dans Cloud Firestore. Voir la réponse de Doug ci-dessous.


Il n'existe actuellement aucun moyen de mettre à jour un seul élément de tableau (ou d'ajouter / supprimer un seul élément) dans Cloud Firestore.

Ce code ici:

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

Cela dit de définir le document à proprietary/docIDtel que sharedWith = [{ who: "[email protected]", when: new Date() }mais de ne pas affecter les propriétés de document existantes. C'est très similaire à l' update()appel que vous avez fourni mais l' set()appel avec crée le document s'il n'existe pas alors que l' update()appel échouera.

Vous avez donc deux options pour réaliser ce que vous voulez.

Option 1 - Définir l'ensemble du tableau

Appelez set()avec tout le contenu du tableau, ce qui nécessitera de lire d'abord les données actuelles de la base de données. Si vous êtes préoccupé par les mises à jour simultanées, vous pouvez faire tout cela dans une transaction.

Option 2 - Utiliser une sous-collection

Vous pouvez créer sharedWithune sous-collection du document principal. Ensuite, l'ajout d'un seul élément ressemblerait à ceci:

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

Bien sûr, cela s'accompagne de nouvelles limitations. Vous ne pourrez pas interroger les documents en fonction des personnes avec lesquelles ils sont partagés, ni obtenir le document et toutes les sharedWithdonnées en une seule opération.

Sam Stern
la source
9
C'est tellement frustrant ... mais merci de m'avoir fait savoir que je ne deviens pas fou.
ItJustWerks
51
c'est un gros inconvénient, Google doit le réparer dès que possible.
Sajith Mantharath
3
La réponse de @ DougGalante indique que cela a été corrigé. Utilisez la arrayUnionméthode.
quicklikerabbit
152

Firestore a maintenant deux fonctions qui vous permettent de mettre à jour un tableau sans réécrire le tout.

Lien: https://firebase.google.com/docs/firestore/manage-data/add-data , en particulier https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array

Mettre à jour les éléments d'un tableau

Si votre document contient un champ de tableau, vous pouvez utiliser arrayUnion () et arrayRemove () pour ajouter et supprimer des éléments. arrayUnion () ajoute des éléments à un tableau mais uniquement des éléments qui ne sont pas déjà présents. arrayRemove () supprime toutes les instances de chaque élément donné.

Doug Galante
la source
56
Existe-t-il un moyen de mettre à jour un index spécifique à partir du tableau?
Artur Carvalho
1
Comment utiliser cette fonction de mise à jour du tableau avec "react-native-firebase"? (Je ne trouve pas cela sur les documents officiels de react-native-firebase)
kernelman
4
@ArturCarvalho Non, la raison est expliquée dans cette vidéo youtube.com/…
Adam
@ArturCarvalho avez-vous trouvé une solution pour mettre à jour un index spécifique à partir du tableau?
Yogendra Patel
3
pour qui doit le faire sur le client, utilisez "import * as firebase from 'firebase / app';" puis "firebase.firestore.FieldValue.arrayUnion (NEW_ELEMENT)"
michelepatrassi
15

Vous pouvez utiliser une transaction ( https://firebase.google.com/docs/firestore/manage-data/transactions ) pour obtenir le tableau, pousser dessus, puis mettre à jour le 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);
    });
Gabriel McCallin
la source
1
À l'instruction if, vous devriez le changer en ceci, car il vous manque l' documentReferenceajout userRefcomme indiqué: transaction.set(userRef, { bookings: [booking] });
Ilir Hushi
8

Voici le dernier exemple de la documentation 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
la source
@nifCody, cela ajoutera en effet un nouvel élément de chaîne "greater_virginia" à un tableau existant "regions". Je l'ai testé avec succès, et certainement pas en ajoutant "objet". Il est en phase avec la question posée: "pousser de nouveaux enregistrements".
Veeresh Devireddy
3

Pour s'appuyer sur la réponse de Sam Stern , il existe également une troisième option qui m'a simplifié les choses et qui utilise ce que Google appelle une carte, qui est essentiellement un dictionnaire.

Je pense qu'un dictionnaire est bien meilleur pour le cas d'utilisation que vous décrivez. J'utilise généralement des tableaux pour des éléments qui ne sont pas vraiment trop mis à jour, donc ils sont plus ou moins statiques. Mais pour les choses qui sont beaucoup écrites, en particulier les valeurs qui doivent être mises à jour pour les champs qui sont liés à autre chose dans la base de données, les dictionnaires s'avèrent beaucoup plus faciles à maintenir et à utiliser.

Donc, pour votre cas spécifique, la structure de la base de données ressemblerait à ceci:

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

Cela vous permettra de faire ce qui suit:

var whoEmail = '[email protected]';

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

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

La raison de la définition de l'objet en tant que variable est que l'utilisation 'sharedWith.' + whoEmail + '.when'directe dans la méthode set entraînera une erreur, du moins lors de son utilisation dans une fonction cloud Node.js.

Horea
la source
2

Autre que les réponses mentionnées ci-dessus. Cela le fera. Utilisation d'Angular 5 et AngularFire2. ou utilisez firebase.firestore () au lieu 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
la source
1

Considérez John Doe comme un document plutôt qu'une collection

Donnez-lui une collection de choses et de choses partagées avec d'autres

Ensuite, vous pouvez mapper et interroger les éléments partagés de John Doe dans cette collection parallèle 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
la source
0

Si quelqu'un recherche une solution Java Firestore SDK pour ajouter des éléments dans le champ du tableau:

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

Pour supprimer des éléments de l'utilisateur de la baie: FieldValue.arrayRemove()

A_01
la source
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
la source
1
Veuillez expliquer votre réponse.
Jenea Vranceanu il y a
En effet, veuillez lire stackoverflow.com/help/how-to-answer
jasie il y a
-1

C'est ainsi que je l'ai fait fonctionner. J'espère que cela vous aidera tous car je le vois comme une solution bien meilleure. Ici, je veux changer le statut de la tâche de ouverte (close = false) à close (close = true), tout en gardant tout le reste identique dans l'objet.

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

et voici le service qui le met à jour

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

return leadRef.update(updateData);
}
Leandrit Ferizi
la source
Salut. Veuillez noter que cette approche présente de nombreux problèmes et risque de perdre / corrompre / inflexibilité des données en raison d'une utilisation malheureuse des tableaux et d'une "mise à jour" non incrémentielle. Vous ne devez pas stocker les tâches dans un tableau. Ayez-les plutôt dans une collection.
KarolDepka
Désolé, j'ai dû voter contre parce qu'il contient de mauvaises pratiques.
KarolDepka