Eseguire join con sottoquery

Sfondo

Le operazioni della pipeline sono una nuova interfaccia di query per Cloud Firestore. Questa interfaccia fornisce funzionalità di query avanzate che includono espressioni complesse. La versione Enterprise di Firestore supporta i join in stile relazionale tramite sottoquery correlate. A differenza di molti database NoSQL che spesso richiedono la denormalizzazione dei dati o l'esecuzione di più richieste lato client, le sottoquery consentono di combinare e aggregare i dati da raccolte o sottoraccolte correlate direttamente sul server.

Le sottoquery sono espressioni che eseguono una pipeline nidificata per ogni documento elaborato dalla query esterna. Ciò consente pattern di recupero dei dati complessi, ad esempio il recupero di un documento insieme agli elementi della sottoraccolta correlata o l'unione di dati collegati logicamente in raccolte root disparate.

Concetti

Questa sezione introduce i concetti di base per l'utilizzo delle sottoquery per eseguire i join nelle operazioni della pipeline.

Sottoquery come espressioni

Una sottoquery non è una fase di primo livello, ma un'espressione che può essere utilizzata in qualsiasi fase che accetta espressioni, ad esempio select(...), add_fields(...), where(...) o sort(...).

Cloud Firestore supporta tre tipi di sottoquery:

  • Sottoquery di array: materializzano l'intero set di risultati della sottoquery come un array di documenti.
  • Sottoquery scalari: restituiscono un singolo valore, ad esempio un conteggio, una media o un campo specifico di un documento correlato.
  • Sottoquery subcollection(...): join semplificati per una relazione padre-figlio uno-a-molti.

Ambito e variabili

Quando scrivi un join, la sottoquery nidificata spesso deve fare riferimento ai campi del documento "esterno" (il documento principale). Per colmare questi ambiti, utilizza la let(...) fase (denominata define(...) in alcuni SDK) per definire le variabili nell'ambito principale a cui è possibile fare riferimento nella sottoquery utilizzando la funzione variable(...).

Sintassi

Le sezioni seguenti forniscono una panoramica della sintassi per l'esecuzione dei join.

La fase let(...)

La fase let(...) (denominata define(...) in alcuni SDK) è una fase di non filtraggio che porta esplicitamente i dati dall'ambito principale a una variabile denominata per l'utilizzo negli ambiti nidificati successivi.

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

Sottoquery di array

Una sottoquery di array è un caso speciale di sottoquery di espressione che materializza l'intero set di risultati della sottoquery in un array. Se la sottoquery restituisce zero righe, restituisce un array vuoto. Non restituisce mai un array null. Queste query sono utili quando i risultati completi sono necessari nel risultato finale, ad esempio quando si materializza una raccolta nidificata o correlata.

Le query possono filtrare, ordinare e aggregare nella sottoquery per ridurre anche la quantità di dati da recuperare e restituire, contribuendo a ridurre il costo della query. L'ordine della sottoquery viene rispettato, il che significa che una fase sort(...) nella sottoquery controlla l'ordine dei risultati nell'array finale.

Utilizza il wrapper SDK toArrayExpression() per convertire una query in un array.

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

Risposta

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

Risposta

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

Risposta

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

Risposta

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

Sottoquery scalari

Le sottoquery scalari vengono spesso utilizzate in una fase select(...) o where(...) per consentire il filtraggio o il risultato di una sottoquery senza materializzare direttamente la query completa.

Una sottoquery scalare che produce zero risultati restituisce null, mentre una sottoquery che restituisce più elementi genera un errore di runtime.

Quando una sottoquery scalare produce un solo campo per risultato, il campo viene elevato per essere il risultato di primo livello della sottoquery. Questo si verifica più comunemente quando la sottoquery termina con un select(field("user_name")) o aggregate(countAll().as("total")) in cui lo schema della sottoquery è un singolo campo. In caso contrario, quando una sottoquery può produrre più campi, questi vengono racchiusi in una mappa.

Utilizza il wrapper SDK toScalarExpression() per convertire una query in un'espressione scalare.

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

Risposta

        {
            "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
        ])
        

Risposta

        {
            "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)
    }
  

Risposta

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

Risposta

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

Sottoquery subcollection(...)

Sebbene sia offerta come fase, la subcollection(...) fase di input consente di eseguire join sul modello di dati gerarchico di Cloud Firestore. In un modello gerarchico, le query spesso devono recuperare un documento insieme ai dati delle relative sottoraccolte. Sebbene sia possibile ottenere questo risultato utilizzando una collection_group(...) fase di input seguita da un filtro sul riferimento principale, subcollection(...) fornisce una sintassi molto più concisa.

Oltre alla condizione di join implicita, questa fase si comporta in modo simile a una sottoquery di array, restituendo un risultato vuoto se non vengono trovati documenti, anche se la raccolta nidificata non esiste.

Si tratta fondamentalmente di zucchero sintattico: utilizza automaticamente __name__ di documento nell'ambito esterno come chiave di join per risolvere la relazione gerarchica. Questo è il modo preferito per eseguire ricerche nelle raccolte collegate in una relazione padre-figlio.

Best practice

  • Gestire la memoria con toArrayExpression(): fai attenzione alle toArrayExpression() sottoquery, poiché la materializzazione di un numero elevato di documenti può esaurire il limite di memoria della query (128 MiB). Per risolvere questo problema, utilizza select(...) all'interno della sottoquery per restituire solo i campi necessari e applica i filtri where(...) per limitare il numero di documenti restituiti. Se appropriato, valuta la possibilità di utilizzare limit(...) per limitare il numero di documenti restituiti dalla sottoquery.
  • Indicizzazione: assicurati che i campi utilizzati nella clausola where(...) di una sottoquery siano indicizzati. I join efficienti si basano sulla possibilità di eseguire ricerche di indici anziché scansioni complete delle tabelle.

Per ulteriori best practice relative alle query, consulta la nostra guida sull'ottimizzazione delle query.

Limitazioni

  • subcollection(...) ambito: la fase di input subcollection(...) è supportata solo all'interno delle sottoquery, in quanto richiede il contesto di un documento principale per risolvere la relazione gerarchica ed eseguire il join.
  • Profondità di nidificazione: le sottoquery possono essere nidificate fino a 20 livelli di profondità.
  • Utilizzo della memoria: il limite di 128 MiB per i dati materializzati si applica all'intera query, inclusi tutti i documenti uniti.