Como atualizar um “array de objetos” com Firestore?

104

No momento, estou testando o Firestore e estou preso em algo muito simples: "atualizar uma matriz (também conhecida como subdocumento)".

Minha estrutura de banco de dados é super simples. Por exemplo:

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

Estou tentando (sem sucesso) inserir novos registros em uma shareWithmatriz de objetos.

Eu tentei:

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

Nenhum funciona. Essas consultas sobrescrevem meu array.

A resposta pode ser simples, mas não consegui encontrar ...

nerotulipa
fonte

Respostas:

71

Editar 13/08/2018 : agora há suporte para operações de array nativas no Cloud Firestore. Veja a resposta de Doug abaixo.


Atualmente, não há como atualizar um único elemento da matriz (ou adicionar / remover um único elemento) no Cloud Firestore.

Este código aqui:

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

Isso diz para definir o documento de proprietary/docIDtal forma, sharedWith = [{ who: "[email protected]", when: new Date() }mas não afetar as propriedades existentes do documento. É muito semelhante à update()chamada que você forneceu, mas a set()chamada com criará o documento se ele não existir enquanto a update()chamada falhará.

Portanto, você tem duas opções para conseguir o que deseja.

Opção 1 - Definir toda a matriz

Chame set()com todo o conteúdo do array, o que exigirá a leitura dos dados atuais do banco de dados primeiro. Se você estiver preocupado com atualizações simultâneas, pode fazer tudo isso em uma transação.

Opção 2 - Use uma subcoleção

Você pode fazer sharedWithuma subcoleção do documento principal. Então, adicionar um único item ficaria assim:

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

Claro que isso vem com novas limitações. Você não poderia consultar documentos com base nas pessoas com quem eles são compartilhados, nem obter o documento e todos os sharedWithdados em uma única operação.

Sam Stern
fonte
9
Isso é tão frustrante ... mas obrigado por me informar que não estou ficando louco.
ItJustWerks
50
esta é uma grande desvantagem, o Google tem que consertá-la o mais rápido possível.
Sajith Mantharath
3
A resposta de @DougGalante indica que isso foi corrigido. Use o arrayUnionmétodo.
quicklikerabbit
152

O Firestore agora tem duas funções que permitem atualizar um array sem reescrever tudo.

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

Atualizar elementos em uma matriz

Se o seu documento contiver um campo de matriz, você pode usar arrayUnion () e arrayRemove () para adicionar e remover elementos. arrayUnion () adiciona elementos a um array, mas apenas elementos ainda não presentes. arrayRemove () remove todas as instâncias de cada elemento fornecido.

Doug Galante
fonte
56
Existe alguma maneira de atualizar um índice específico da matriz?
Artur Carvalho
1
Como usar esse recurso de atualização de array com "react-native-firebase"? (Não consigo encontrar isso nos documentos oficiais do
react
4
@ArturCarvalho Não, o motivo é explicado neste vídeo youtube.com/…
Adam
@ArturCarvalho você encontrou alguma solução para atualizar um índice específico do array?
Yogendra Patel
3
para quem precisa fazer isso no cliente, use "import * as firebase from 'firebase / app';" em seguida, "firebase.firestore.FieldValue.arrayUnion (NEW_ELEMENT)"
michelepatrassi
15

Você pode usar uma transação ( https://firebase.google.com/docs/firestore/manage-data/transactions ) para obter o array, enviar para ele e, em seguida, atualizar o 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
fonte
1
Na declaração if, você deve alterá-la para esta, porque está faltando o documentReferenceadd userRefcomo mostrado: transaction.set(userRef, { bookings: [booking] });
Ilir Hushi
8

Aqui está o exemplo mais recente da documentação do 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
fonte
@nifCody, isso realmente adicionará um novo elemento string "maior_virginia" a uma matriz existente "regiões". Eu testei com sucesso e definitivamente não adicionei "objeto". Está em sincronia com a pergunta declarada: "enviar novos registros".
Veeresh Devireddy
3

Para desenvolver a resposta de Sam Stern , há também uma terceira opção que tornou as coisas mais fáceis para mim: usar o que o Google chama de mapa, que é essencialmente um dicionário.

Acho que um dicionário é muito melhor para o caso de uso que você está descrevendo. Eu normalmente uso arrays para coisas que não são muito atualizadas, então eles são mais ou menos estáticos. Mas para coisas que são muito gravadas, especificamente valores que precisam ser atualizados para campos que estão vinculados a alguma outra coisa no banco de dados, os dicionários são muito mais fáceis de manter e trabalhar.

Portanto, para seu caso específico, a estrutura do banco de dados seria assim:

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

Isso permitirá que você faça o seguinte:

var whoEmail = '[email protected]';

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

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

A razão para definir o objeto como uma variável é que usar 'sharedWith.' + whoEmail + '.when'diretamente no método set resultará em um erro, pelo menos ao usá-lo em uma função de nuvem Node.js.

Horea
fonte
2

Além das respostas mencionadas acima. Isso vai bastar. Usando Angular 5 e AngularFire2. ou use firebase.firestore () em vez 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
fonte
1

Considere John Doe um documento em vez de uma coleção

Dê a ele uma coleção de coisas compartilhadas com outras pessoas

Em seguida, você pode mapear e consultar as coisas compartilhadas de John Doe nessa coleção paralela das coisas compartilhadas com os outros.

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
fonte
0

Se alguém estiver procurando por uma solução Java Firestore SDK para adicionar itens no campo array:

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 excluir itens do usuário da matriz: FieldValue.arrayRemove()

A_01
fonte
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
fonte
-1

É assim que eu fiz funcionar. Espero que ajude a todos, pois vejo isso como uma solução muito melhor. Aqui, desejo alterar o status da tarefa de aberta (fechar = falso) para fechar (fechar = verdadeiro), mantendo todo o resto igual no 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);
    }

e aqui está o serviço que realmente o atualiza

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

return leadRef.update(updateData);
}
Leandrit Ferizi
fonte
Oi. Esteja ciente de que esta abordagem tem muitos problemas e riscos de perda / corrupção / inflexibilidade de dados devido ao uso infeliz de matrizes e uma "atualização" não incremental. Você não deve armazenar as tarefas em uma matriz. Em vez disso, coloque-os em uma coleção.
KarolDepka,
Desculpe, eu tive que votar negativamente porque tem práticas ruins nele.
KarolDepka