Google Firestore - como obter documentos por vários ids em uma viagem de ida e volta?


98

Gostaria de saber se é possível obter vários documentos por lista de ids em uma viagem de ida e volta (chamada de rede) para o Firestore.


4
Você parece presumir que as viagens de ida e volta estão causando problemas de desempenho em seu aplicativo. Eu não suporia isso. O Firebase tem um histórico de bom desempenho nesses casos, pois canaliza as solicitações . Embora eu não tenha verificado como o Firestore se comporta neste cenário, adoraria ver a prova de um problema de desempenho antes de presumir que ele existe.
Frank van Puffelen

1
Vamos dizer que eu preciso de documentos a, b, cfazer alguma coisa. I pedidos para todos os três em paralelo em pedidos separados. aleva 100ms, bleva 150ms e cleva 3000ms. Como resultado, preciso esperar 3000ms para fazer a tarefa. Vai ser maxdeles. Vai ser mais arriscado quando o número de documentos a buscar é grande. Depende do status da rede, acho que isso pode se tornar um problema.
Joon

1
No SELECT * FROM docs WHERE id IN (a,b,c)entanto, enviar todos juntos não levaria o mesmo tempo? Não vejo diferença, já que a conexão é estabelecida uma vez e o resto é canalizado sobre isso. O tempo (após o estabelecimento inicial da conexão) é o tempo de carregamento de todos os documentos + 1 ida e volta, o mesmo para ambas as abordagens. Se ele se comportar de forma diferente para você, você pode compartilhar uma amostra (como na minha pergunta vinculada)?
Frank van Puffelen

Acho que perdi você. Quando você diz que está em pipeline, quer dizer que o Firestore agrupa e envia consultas automaticamente ao servidor em uma viagem de ida e volta ao banco de dados?
Joon

Para sua informação, o que quero dizer com uma viagem de ida e volta é uma chamada de rede para o banco de dados do cliente. Estou perguntando se várias consultas são agrupadas automaticamente como uma viagem de ida e volta pelo Firestore ou se várias consultas são realizadas como várias viagens de ida e volta em paralelo.
Joon

Respostas:


88

se você estiver dentro do Node:

https://github.com/googleapis/nodejs-firestore/blob/master/dev/src/index.ts#L701

/**
* Retrieves multiple documents from Firestore.
*
* @param {...DocumentReference} documents - The document references
* to receive.
* @returns {Promise<Array.<DocumentSnapshot>>} A Promise that
* contains an array with the resulting document snapshots.
*
* @example
* let documentRef1 = firestore.doc('col/doc1');
* let documentRef2 = firestore.doc('col/doc2');
*
* firestore.getAll(documentRef1, documentRef2).then(docs => {
*   console.log(`First document: ${JSON.stringify(docs[0])}`);
*   console.log(`Second document: ${JSON.stringify(docs[1])}`);
* });
*/

Isso é especificamente para o SDK do servidor

ATUALIZAÇÃO: "Cloud Firestore [client-side sdk] agora oferece suporte a consultas IN!"

https://firebase.googleblog.com/2019/11/cloud-firestore-now-supports-in-queries.html

myCollection.where(firestore.FieldPath.documentId(), 'in', ["123","456","789"])


28
Para quem deseja chamar este método com uma matriz gerada dinamicamente de referências de documento, você pode fazer assim: firestore.getAll (... arrayOfReferences) .then ()
Horea

1
Sinto muito @KamanaKisinga ... Eu não fiz nenhum firebase em quase um ano e realmente não posso ajudar neste momento (olha, eu na verdade postei esta resposta um ano atrás hoje!)
Nick Franceschina

2
Os SDKs do lado do cliente agora também oferecem essa funcionalidade. veja a resposta de jeodonara para um exemplo: stackoverflow.com/a/58780369
Frank van Puffelen

4
aviso: o filtro de entrada está limitado a 10 itens atualmente. Portanto, você provavelmente descobrirá que é inútil quando estiver prestes a entrar em produção.
Martin Cremer

6
na verdade você precisa usar firebase.firestore.FieldPath.documentId()e não'id'
Maddocks

20

Eles acabaram de anunciar essa funcionalidade, https://firebase.googleblog.com/2019/11/cloud-firestore-now-supports-in-queries.html .

Agora você pode usar consultas como, mas lembre-se de que o tamanho da entrada não pode ser maior que 10.

userCollection.where('uid', 'in', ["1231","222","2131"])


Há uma consulta whereIn em vez de onde. E eu não sei como projetar consulta para vários documentos de uma lista de ids de documentos que pertencem a uma coleção específica. Por favor ajude.
Erro de compilação final de

16
@Compileerrorend você poderia tentar isso? db.collection('users').where(firebase.firestore.FieldPath.documentId(), 'in',["123","345","111"]).get()
jeadonara

obrigado, especialmente porfirebase.firestore.FieldPath.documentId()
Cherniv

10

Não, no momento não há como agrupar várias solicitações de leitura usando o SDK do Cloud Firestore e, portanto, não há como garantir que você possa ler todos os dados de uma vez.

No entanto, como Frank van Puffelen disse nos comentários acima, isso não significa que buscar 3 documentos será 3x mais lento do que buscar um documento. É melhor realizar suas próprias medições antes de chegar a uma conclusão aqui.


1
O fato é que eu quero saber os limites teóricos do desempenho do Firestore antes de migrar para o Firestore. Não quero migrar e depois perceber que não é bom o suficiente para o meu caso de uso.
Joon,

2
Olá, também há uma consideração de cose aqui. Digamos que eu tenha armazenado a lista de todos os IDs do meu amigo e o número é 500. Posso obter a lista com um custo de 1 leitura, mas para exibir seu Nome e fotoURL, isso me custará 500 leituras.
Tapas Mukherjee

1
Se você está tentando ler 500 documentos, são necessárias 500 leituras. Se você combinar as informações de que precisa de todos os 500 documentos em um único documento extra, bastará uma leitura. Isso é chamado de tipo de duplicação de dados, é bastante normal na maioria dos bancos de dados NoSQL, incluindo o Cloud Firestore.
Frank van Puffelen

1
@FrankvanPuffelen Por exemplo, em mongoDb, você pode usar ObjectId como stackoverflow.com/a/32264630/648851 .
Sitian Liu

2
Como @FrankvanPuffelen disse, a duplicação de dados é bastante comum no banco de dados NoSQL. Aqui você deve se perguntar com que frequência esses dados devem ser lidos e o quão atualizados eles precisam estar. Se você armazenar 500 informações de usuários, digamos seu nome + foto + identificação, poderá obtê-los em uma leitura. Mas se você precisar atualizá-los, provavelmente terá que usar uma função de nuvem para atualizar essas referências cada vez que um usuário atualizar seu nome / foto, portanto, executando uma função de nuvem + fazendo algumas operações de gravação. Não há implementação "certa" / "melhor", apenas depende do seu caso de uso.
schankam

9

Na prática, você usaria firestore.getAll assim

async getUsers({userIds}) {
    const refs = userIds.map(id => this.firestore.doc(`users/${id}`))
    const users = await this.firestore.getAll(...refs)
    console.log(users.map(doc => doc.data()))
}

ou com sintaxe de promessa

getUsers({userIds}) {
    const refs = userIds.map(id => this.firestore.doc(`users/${id}`))
    this.firestore.getAll(...refs).then(users => console.log(users.map(doc => doc.data())))
}

2
esta realmente deve ser a resposta selecionada porque permite que você use mais de 10 ids
sshah98

9

Você poderia usar uma função como esta:

function getById (path, ids) {
  return firestore.getAll(
    [].concat(ids).map(id => firestore.doc(`${path}/${id}`))
  )
}

Ele pode ser chamado com um único ID:

getById('collection', 'some_id')

ou uma matriz de IDs:

getById('collection', ['some_id', 'some_other_id'])

5

Certamente, a melhor maneira de fazer isso é implementando a consulta real do Firestore em um Cloud Function. Haveria então apenas uma única chamada de ida e volta do cliente para o Firebase, que parece ser o que você está pedindo.

Você realmente deseja manter toda a sua lógica de acesso a dados como este lado do servidor de qualquer maneira.

Internamente, provavelmente haverá o mesmo número de chamadas para o próprio Firebase, mas todas elas seriam através das interconexões super-rápidas do Google, ao invés da rede externa, e combinadas com o pipelining que Frank van Puffelen explicou, você deve obter um excelente desempenho esta abordagem.


3
Armazenar a implementação em um Cloud Function é a decisão certa em alguns casos em que você tem uma lógica complexa, mas provavelmente não no caso em que deseja apenas mesclar uma lista com vários IDs. O que você perde é o cache do lado do cliente e a formatação de retorno padronizada de chamadas regulares. Isso causou mais problemas de desempenho do que resolveu em alguns casos em meus aplicativos quando usei a abordagem.
Jeremiah

3

Se estiver usando flutter, você pode fazer o seguinte:

Firestore.instance.collection('your collection name').where(FieldPath.documentId, whereIn:[list containing multiple document IDs]).getDocuments();

Isso retornará um Future contendo o List<DocumentSnapshot>qual você pode iterar conforme achar adequado.


2

Veja como você faria algo assim em Kotlin com o Android SDK.
Pode não ser necessariamente em uma viagem de ida e volta, mas efetivamente agrupa o resultado e evita muitos retornos de chamada aninhados.

val userIds = listOf("123", "456")
val userTasks = userIds.map { firestore.document("users/${it!!}").get() }

Tasks.whenAllSuccess<DocumentSnapshot>(userTasks).addOnSuccessListener { documentList ->
    //Do what you need to with the document list
}

Observe que buscar documentos específicos é muito melhor do que buscar todos os documentos e filtrar o resultado. Isso ocorre porque o Firestore cobra pelo conjunto de resultados da consulta.


1
Funciona muito bem, exatamente o que eu estava procurando!
Georgi

0

Isso não parece ser possível no Firestore no momento. Não entendo por que a resposta de Alexander é aceita, a solução que ele propõe apenas retorna todos os documentos da coleção "usuários".

Dependendo do que você precisa fazer, você deve tentar duplicar os dados relevantes que você precisa exibir e só solicitar um documento completo quando necessário.


0

O melhor que você pode fazer é não usar Promise.allcomo seu cliente, então deve esperar pelas .allleituras antes de continuar.

Repita as leituras e deixe-as resolver de forma independente. No lado do cliente, isso provavelmente se resume à interface do usuário com várias imagens do carregador de progresso resolvidas para valores independentemente. No entanto, isso é melhor do que congelar todo o cliente até.all as leituras sejam resolvidas.

Portanto, despeje todos os resultados síncronos na exibição imediatamente e deixe os resultados assíncronos chegarem conforme eles são resolvidos, individualmente. Isso pode parecer uma distinção mesquinha, mas se o seu cliente não tiver conectividade com a Internet (como eu tenho atualmente nesta cafeteria), congelar toda a experiência do cliente por vários segundos provavelmente resultará em uma experiência 'este aplicativo é uma merda'.


2
É assíncrono, há muitos casos de uso para usar Promise.all... ele não precisa necessariamente "congelar" nada - você pode precisar esperar por todos os dados antes de fazer algo significativo
Ryan Taylor

Existem vários casos de uso em que você precisa carregar todos os seus dados, portanto, a espera (como um botão giratório com uma mensagem apropriada, sem necessidade de "congelar" qualquer IU como você diz) pode ser totalmente necessária por Promise.all .. Isso realmente depende do tipo de produto que você está construindo aqui. Esse tipo de comentário é, para minha opinião, muito irrelevante e não deveria conter nenhuma palavra "melhor". Realmente depende de cada caso de uso diferente que alguém possa enfrentar e do que seu aplicativo está fazendo pelo usuário.
schankam

0

Espero que isso ajude você, funciona para mim.

getCartGoodsData(id) {

    const goodsIDs: string[] = [];

    return new Promise((resolve) => {
      this.fs.firestore.collection(`users/${id}/cart`).get()
        .then(querySnapshot => {
          querySnapshot.forEach(doc => {
            goodsIDs.push(doc.id);
          });

          const getDocs = goodsIDs.map((id: string) => {
            return this.fs.firestore.collection('goods').doc(id).get()
              .then((docData) => {
                return docData.data();
              });
          });

          Promise.all(getDocs).then((goods: Goods[]) => {
            resolve(goods);
          });
        });
    });
  }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.