Arrière-plan
Les opérations de pipeline constituent une nouvelle interface de requête pour Cloud Firestore. Cette interface fournit des fonctionnalités de requête avancées, y compris des expressions complexes. L'édition Firestore Enterprise est compatible avec les jointures de style relationnel via des sous-requêtes corrélées. Contrairement à de nombreuses bases de données NoSQL qui nécessitent souvent de dénormaliser les données ou d'effectuer plusieurs requêtes côté client, les sous-requêtes vous permettent de combiner et d'agréger des données provenant de collections ou de sous-collections associées directement sur le serveur.
Les sous-requêtes sont des expressions qui exécutent un pipeline imbriqué pour chaque document traité par la requête externe. Cela permet d'obtenir des modèles de récupération de données complexes, par exemple en récupérant un document avec les éléments de sa sous-collection associée ou en joignant des données liées logiquement dans des collections racines disparates.
Concepts
Cette section présente les concepts de base liés à l'utilisation de sous-requêtes pour effectuer des jointures dans les opérations de pipeline.
Sous-requêtes en tant qu'expressions
Une sous-requête n'est pas une étape de premier niveau. Il s'agit plutôt d'une expression qui
peut être utilisée dans n'importe quelle étape acceptant des expressions, telles que
select(...),
add_fields(...),
where(...) ou sort(...).
Cloud Firestore est compatible avec trois types de sous-requêtes :
- Sous-requêtes de tableau : matérialisent l'ensemble des résultats de la sous-requête sous forme de tableau de documents.
- Sous-requêtes scalaires : sont évaluées à une seule valeur, telle qu'un nombre, une moyenne ou un champ spécifique d'un document associé.
- Sous-requêtes
subcollection(...): jointures simplifiées pour une relation parent-enfant de type un à plusieurs.
Champ d'application et variables
Lors de l'écriture d'une jointure, la sous-requête imbriquée doit souvent référencer des champs du document "externe" (le parent). Pour relier ces champs d'application, utilisez l'étape
let(...) (appelée define(...) dans certains
SDK) afin de définir des variables dans le champ d'application parent, qui peuvent ensuite être référencées dans la
sous-requête à l'aide de la fonction variable(...).
Syntaxe
Les sections suivantes présentent la syntaxe permettant d'effectuer des jointures.
Étape let(...)
L'étape let(...) (appelée define(...) dans certains
SDK) est une étape sans filtrage qui importe explicitement des données du champ d'application parent
dans une variable nommée pour une utilisation dans des champs d'application imbriqués ultérieurs.
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); }
Sous-requêtes de tableau
Une sous-requête de tableau est un cas particulier de sous-requête d'expression qui matérialise l'ensemble des résultats de la sous-requête dans un tableau. Si la sous-requête ne renvoie aucune ligne, elle est évaluée à un tableau vide. Elle ne renvoie jamais de tableau null. Ces requêtes sont utiles lorsque les résultats complets sont requis dans le résultat final, par exemple lors de la matérialisation d'une collection imbriquée ou corrélée.
Les requêtes peuvent filtrer, trier et agréger dans la sous-requête afin de réduire la quantité de données à récupérer et à renvoyer, ce qui permet de réduire le coût de la requête. L'ordre de la sous-requête est respecté, ce qui signifie qu'une étape sort(...) dans la sous-requête contrôle l'ordre des résultats dans le tableau final.
Utilisez le wrapper SDK toArrayExpression() pour convertir une requête en tableau.
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"
});
}
Réponse
{
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" }); }
Réponse
{ 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) }
Réponse
{ 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); }
Réponse
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Sous-requêtes scalaires
Les sous-requêtes scalaires sont souvent utilisées dans une étape select(...) ou
where(...) pour permettre de filtrer ou de générer le
résultat d'une sous-requête sans matérialiser directement la requête complète.
Une sous-requête scalaire qui ne produit aucun résultat est évaluée à null, tandis qu'une sous-requête qui est évaluée à plusieurs éléments génère une erreur d'exécution.
Lorsqu'une sous-requête scalaire ne produit qu'un seul champ par résultat, le champ est élevé pour devenir le résultat de premier niveau de la sous-requête. Cela se produit le plus souvent
lorsque la sous-requête se termine par un select(field("user_name")) ou
aggregate(countAll().as("total")) où le schéma de la sous-requête n'est qu'un
seul champ. Sinon, lorsqu'une sous-requête peut produire plusieurs champs, ils sont encapsulés dans une carte.
Utilisez le wrapper SDK toScalarExpression() pour convertir une requête en expression scalaire.
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
});
}
Réponse
{
"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 ])
Réponse
{ "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) }
Réponse
{ "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); }
Réponse
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Sous-requêtes subcollection(...)
Bien qu'elle soit proposée en tant qu'étape, l'
subcollection(...) étape d'entrée permet
d'effectuer des jointures sur le modèle de données hiérarchique de Cloud Firestore. Dans un modèle hiérarchique, les requêtes doivent souvent récupérer un document avec les données de ses propres sous-collections. Bien que vous puissiez y parvenir à l'aide d'une
collection_group(...) étape d'entrée suivie
d'un filtre sur la référence parent, subcollection(...) fournit une syntaxe beaucoup plus
concise.
Outre la condition de jointure implicite, cela fonctionne de la même manière qu'une sous-requête de tableau, en renvoyant un résultat vide si aucun document ne correspond, même si la collection imbriquée n'existe pas.
Il s'agit fondamentalement d'un sucre syntaxique : il utilise automatiquement le __name__ de
le document dans le champ d'application externe comme clé de jointure pour résoudre la relation hiérarchique. Il s'agit donc de la méthode privilégiée pour effectuer des recherches dans les collections liées dans une relation parent-enfant.
Bonnes pratiques
- Gérer la mémoire avec
toArrayExpression(): soyez prudent avectoArrayExpression()sous-requêtes, car la matérialisation d'un grand nombre de documents peut épuiser la limite de mémoire de la requête (128 Mio). Pour éviter cela, utilisezselect(...)dans la sous-requête afin de ne renvoyer que les champs nécessaires et appliquez des filtreswhere(...)pour limiter le nombre de documents renvoyés. Envisagez d'utiliserlimit(...)si nécessaire pour limiter le nombre de documents renvoyés par la sous-requête. - Indexation : assurez-vous que les champs utilisés dans la clause
where(...)d'une sous-requête sont indexés. Les jointures performantes reposent sur la possibilité d'effectuer des recherches d'index plutôt que des analyses de table complètes.
Pour en savoir plus sur les bonnes pratiques concernant les requêtes, consultez notre guide sur l'optimisation des requêtes.
Limites
subcollection(...)champ d'application : l'étape d'entréesubcollection(...)n'est compatible qu'avec les sous-requêtes, car elle nécessite le contexte d'un document parent pour résoudre la relation hiérarchique et effectuer la jointure.- Profondeur d'imbrication : les sous-requêtes peuvent être imbriquées jusqu'à 20 niveaux de profondeur.
- Utilisation de la mémoire : la limite de 128 Mio pour les données matérialisées s'applique à l'ensemble de la requête, y compris à tous les documents joints.