Fazer junções com subconsultas

Contexto

As operações de pipeline são uma nova interface de consulta para Cloud Firestore. Essa interface oferece funcionalidades avançadas de consulta, incluindo expressões complexas. A edição Enterprise do Firestore oferece suporte a junções de estilo relacional por meio de subconsultas correlacionadas. Ao contrário de muitos bancos de dados NoSQL que geralmente exigem a desnormalização de dados ou a execução de várias solicitações do lado do cliente, as subconsultas permitem combinar e agregar dados de coleções ou subcoleções relacionadas diretamente no servidor.

As subconsultas são expressões que executam um pipeline aninhado para cada documento processado pela consulta externa. Isso permite padrões complexos de recuperação de dados, como buscar um documento junto com os itens da subcoleção relacionada ou unir dados logicamente vinculados em coleções raiz diferentes.

Conceitos

Esta seção apresenta os principais conceitos por trás do uso de subconsultas para realizar junções em operações de pipeline.

Subconsultas como expressões

Uma subconsulta não é um estágio de nível superior. Em vez disso, é uma expressão que pode ser usada em qualquer estágio que aceite expressões, como select(...), add_fields(...), where(...), ou sort(...).

Cloud Firestore oferece suporte a três tipos de subconsultas:

  • Subconsultas de matriz:materializam todo o conjunto de resultados da subconsulta como uma matriz de documentos.
  • Subconsultas escalares:avaliam um único valor, como uma contagem, uma média ou um campo específico de um documento relacionado.
  • Subconsultas subcollection(...):junções simplificadas para uma relação pai-filho um para muitos.

Escopo e variáveis

Ao escrever uma junção, a subconsulta aninhada geralmente precisa fazer referência a campos do documento "externo" (o pai). Para unir esses escopos, use o let(...) estágio (referido como define(...) em alguns SDKs) para definir variáveis no escopo pai que podem ser referenciadas na subconsulta usando a variable(...) função.

Sintaxe

As seções a seguir oferecem uma visão geral da sintaxe para realizar junções.

O estágio let(...)

O estágio let(...) (referido como define(...) em alguns SDKs) é um estágio não filtrado que traz explicitamente dados do escopo pai para uma variável nomeada para uso em escopos aninhados subsequentes.

Web

    async function defineStageData() {
      await setDoc(doc(collection(db, "Authors"), "author_123"), {
        "id": "author_123",
        "name": "Jane Austen"
      });
    }
    
Swift
      func defineStageData() async throws {
      try await db.collection("authors").document("author_123").setData([
        "id": "author_123",
        "name": "Jane Austen"
      ])
    }
    

Kotlin

    fun defineStageData() {
        val author = hashMapOf(
            "id" to "author_123",
            "name" to "Jane Austen",
        )

        db.collection("Authors").document("author_123").set(author)
    }
  

Java

    public void defineStageData() {
        Map<String, Object> author = new HashMap<>();
        author.put("id", "author_123");
        author.put("name", "Jane Austen");

        db.collection("Authors").document("author_123").set(author);
    }
  

Subconsultas de matriz

Uma subconsulta de matriz é um caso especial de subconsulta de expressão que materializa todo o conjunto de resultados da subconsulta em uma matriz. Se a subconsulta não retornar nenhuma linha, ela será avaliada como uma matriz vazia. Ela nunca retorna uma matriz null. Essas consultas são úteis quando os resultados completos são necessários no resultado final, como ao materializar uma coleção aninhada ou correlacionada.

As consultas podem filtrar, classificar e agregar na subconsulta para reduzir a quantidade de dados que precisam ser buscados e retornados, o que ajuda a reduzir o custo da consulta. A ordem da subconsulta é respeitada, o que significa que um estágio sort(...) na subconsulta controla a ordem dos resultados na matriz final.

Use o wrapper do SDK toArrayExpression() para converter uma consulta em uma matriz.

Web

    async function toArrayExpressionStageData() {
      await setDoc(doc(collection(db, "Projects"), "project_1"), {
        "id": "project_1",
        "name": "Alpha Build"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "System Architecture"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "Database Schema Design"
      });
    }
  

Resposta

    {
        id: "project_1",
        name: "Alpha Build",
        taskTitles: [
          "System Architecture", "Database Schema Design"
      ]
    }
    
Swift
    async function toArrayExpressionStageData() {
      await setDoc(doc(collection(db, "Projects"), "project_1"), {
        "id": "project_1",
        "name": "Alpha Build"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "System Architecture"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "Database Schema Design"
      });
    }
    

Resposta

    {
      id: "project_1",
      name: "Alpha Build",
      taskTitles: [
        "System Architecture", "Database Schema Design"
      ]
    }
    

Kotlin

    fun toArrayExpressionData() {
        val project = hashMapOf(
            "id" to "project_1",
            "name" to "Alpha Build",
        )
        db.collection("Projects").document("project_1").set(project)

        val task1 = hashMapOf(
            "project_id" to "project_1",
            "title" to "System Architecture",
        )
        db.collection("Tasks").add(task1)

        val task2 = hashMapOf(
            "project_id" to "project_1",
            "title" to "Database Schema Design",
        )
        db.collection("Tasks").add(task2)
    }
  

Resposta

    {
      id: "project_1",
      name: "Alpha Build",
      taskTitles: [
        "System Architecture", "Database Schema Design"
      ]
    }
    

Java

      public void toArrayExpressionData() {
        Map<String, Object> project = new HashMap<>();
        project.put("id", "project_1");
        project.put("name", "Alpha Build");
        db.collection("Projects").document("project_1").set(project);

        Map<String, Object> task1 = new HashMap<>();
        task1.put("project_id", "project_1");
        task1.put("title", "System Architecture");
        db.collection("Tasks").add(task1);

        Map<String, Object> task2 = new HashMap<>();
        task2.put("project_id", "project_1");
        task2.put("title", "Database Schema Design");
        db.collection("Tasks").add(task2);
    }
  

Resposta

    {
        id: "project_1",
        name: "Alpha Build",
        taskTitles: [
          "System Architecture", "Database Schema Design"
      ]
    }
    

Subconsultas escalares

As subconsultas escalares são frequentemente usadas em um estágio select(...) ou where(...), pois permitem filtrar ou resultar o resultado de uma subconsulta sem materializar a consulta completa diretamente.

Uma subconsulta escalar que produz zero resultados será avaliada como null, enquanto uma subconsulta que é avaliada como vários elementos resultará em um erro de execução.

Quando uma subconsulta escalar produz apenas um único campo por resultado, o campo é elevado para ser o resultado de nível superior da subconsulta. Isso é mais comum quando a subconsulta termina com um select(field("user_name")) ou aggregate(countAll().as("total")) em que o esquema da subconsulta é apenas um único campo. Caso contrário, quando uma subconsulta pode produzir vários campos, eles são encapsulados em um mapa.

Use o wrapper do SDK toScalarExpression() para converter uma consulta em uma expressão escalar.

Web

            async function toScalarExpressionStageData() {
              await setDoc(doc(collection(db, "Authors"), "author_202"), {
                "id": "author_202",
                "name": "Charles Dickens"
              });
              await addDoc(collection(db, "Books"), {
                "author_id": "author_202",
                "title": "Great Expectations",
                "rating": 4.8
              });
              await addDoc(collection(db, "Books"), {
                "author_id": "author_202",
                "title": "Oliver Twist",
                "rating": 4.5
              });
            }
        

Resposta

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      
Swift
        try await db.collection("authors").document("author_202").setData([
          "id": "author_202",
          "name": "Charles Dickens"
        ])
        try await db.collection("books").document().setData([
          "author_id": "author_202",
          "title": "Great Expectations",
          "rating": 4.8
        ])
        try await db.collection("books").document().setData([
          "author_id": "author_202",
          "title": "Oliver Twist",
          "rating": 4.5
        ])
        

Resposta

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      

Kotlin

        fun toScalarExpressionData() {
        val author = hashMapOf(
            "id" to "author_202",
            "name" to "Charles Dickens",
        )
        db.collection("Authors").document("author_202").set(author)

        val book1 = hashMapOf(
            "author_id" to "author_202",
            "title" to "Great Expectations",
            "rating" to 4.8,
        )
        db.collection("Books").add(book1)

        val book2 = hashMapOf(
            "author_id" to "author_202",
            "title" to "Oliver Twist",
            "rating" to 4.5,
        )
        db.collection("Books").add(book2)
    }
  

Resposta

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      

Java

       public void toScalarExpressionData() {
        Map<String, Object> author = new HashMap<>();
        author.put("id", "author_202");
        author.put("name", "Charles Dickens");
        db.collection("Authors").document("author_202").set(author);

        Map<String, Object> book1 = new HashMap<>();
        book1.put("author_id", "author_202");
        book1.put("title", "Great Expectations");
        book1.put("rating", 4.8);
        db.collection("Books").add(book1);

        Map<String, Object> book2 = new HashMap<>();
        book2.put("author_id", "author_202");
        book2.put("title", "Oliver Twist");
        book2.put("rating", 4.5);
        db.collection("Books").add(book2);
    }
  

Resposta

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      

Subconsultas subcollection(...)

Embora oferecido como um estágio, o subcollection(...) estágio de entrada permite realizar junções no modelo de dados hierárquico do Cloud Firestore. Em um modelo hierárquico, as consultas geralmente precisam recuperar um documento junto com dados das próprias subcoleções. Embora seja possível fazer isso usando um collection_group(...) estágio de entrada seguido por um filtro na referência pai, subcollection(...) oferece uma sintaxe muito mais concisa.

Além da condição de junção implícita, isso funciona de maneira semelhante a uma subconsulta de matriz, retornando um resultado vazio se nenhum documento for correspondente, mesmo que a coleção aninhada não exista.

É fundamentalmente um açúcar sintático: ele usa automaticamente o __name__ de o documento no escopo externo como a chave de junção para resolver a relação hierárquica. Isso o torna a maneira preferida de realizar pesquisas em coleções vinculadas em uma relação pai-filho.

Práticas recomendadas

  • Gerenciar a memória com toArrayExpression(): tenha cuidado com toArrayExpression() subconsultas, já que a materialização de um grande número de documentos pode esgotar o limite de memória da consulta (128 MiB). Para atenuar isso, use select(...) na subconsulta para retornar apenas os campos necessários e aplique where(...) filtros para limitar o número de documentos retornados. Considere usar limit(...), se apropriado, para limitar o número de documentos retornados pela subconsulta.
  • Indexação: verifique se os campos usados na cláusula where(...) de uma subconsulta estão indexados. As junções de desempenho dependem da capacidade de realizar buscas de índice em vez de verificações completas de tabelas.

Para mais práticas recomendadas de consulta, consulte nosso guia sobre otimização de consultas.

Limitações

  • subcollection(...) escopo: O subcollection(...) estágio de entrada só é compatível com subconsultas, já que exige o contexto de um documento pai para resolver a relação hierárquica e realizar a junção.
  • Profundidade de aninhamento:as subconsultas podem ser aninhadas em até 20 camadas de profundidade.
  • Uso de memória:o limite de 128 MiB em dados materializados se aplica a toda a consulta, incluindo todos os documentos unidos.