Фон
Конвейерные операции — это новый интерфейс запросов для Cloud Firestore . Этот интерфейс предоставляет расширенные возможности запросов, включая сложные выражения. Firestore Enterprise Edition поддерживает соединения в реляционном стиле с помощью коррелированных подзапросов . В отличие от многих баз данных NoSQL, которые часто требуют денормализации данных или выполнения множества запросов на стороне клиента, подзапросы позволяют объединять и агрегировать данные из связанных коллекций или подколлекций непосредственно на сервере.
Подзапросы — это выражения, которые выполняют вложенный конвейер для каждого документа, обрабатываемого внешним запросом. Это позволяет создавать сложные схемы извлечения данных, такие как получение документа вместе с соответствующими элементами подколлекции или объединение логически связанных данных из разрозненных корневых коллекций.
Концепции
В этом разделе представлены основные концепции использования подзапросов для выполнения объединений в операциях конвейера.
Подзапросы как выражения
Подзапрос не является этапом верхнего уровня; вместо этого это выражение , которое можно использовать на любом этапе, принимающем выражения, такие как select(...) , add_fields(...) , where(...) или sort(...) .
Cloud Firestore поддерживает три типа подзапросов:
- Подзапросы с массивами: Преобразуют весь результирующий набор подзапроса в массив документов.
- Скалярные подзапросы: вычисляют одно значение, например, количество, среднее значение или значение из определенного поля связанного документа.
-
subcollection(...)Подзапросы: упрощенные объединения для отношения «один ко многим» родитель-потомок.
Область применения и переменные
При написании оператора JOIN вложенный подзапрос часто должен ссылаться на поля из «внешнего» документа (родительского). Для связи этих областей видимости используется этап let(...) (в некоторых SDK он называется define(...) ), который определяет переменные в родительской области видимости, на которые затем можно ссылаться в подзапросе с помощью функции variable(...) .
Синтаксис
В следующих разделах представлен обзор синтаксиса для выполнения объединений (joins).
Этап let(...)
Этап let(...) (в некоторых SDK он называется define(...) ) — это этап без фильтрации, который явно переносит данные из родительской области видимости в именованную переменную для использования в последующих вложенных областях видимости.
Web
async function defineStageData() {
await setDoc(doc(collection(db, "Authors"), "author_123"), {
"id": "author_123",
"name": "Jane Austen"
});
}
Быстрый
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); }
Подзапросы массивов
Подзапрос типа «массив» — это частный случай подзапроса типа «выражение», который преобразует весь результирующий набор подзапроса в массив. Если подзапрос возвращает ноль строк, он вычисляется как пустой массив. Он никогда не возвращает null массив. Такие запросы полезны, когда в конечном результате требуются полные данные, например, при материализации вложенной или коррелированной коллекции.
В подзапросах можно выполнять фильтрацию, сортировку и агрегирование, что также позволяет уменьшить объем данных, которые необходимо получить и вернуть, и тем самым снизить стоимость запроса. Порядок подзапроса соблюдается, то есть этап sort(...) в подзапросе контролирует порядок результатов в итоговом массиве.
Используйте обертку SDK toArrayExpression() для преобразования запроса в массив.
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"
});
}
Ответ
{
id: "project_1",
name: "Alpha Build",
taskTitles: [
"System Architecture", "Database Schema Design"
]
}
Быстрый
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" }); }
Ответ
{ 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) }
Ответ
{ 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); }
Ответ
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Скалярные подзапросы
Скалярные подзапросы часто используются на этапе select(...) или where(...) , поскольку позволяют фильтровать данные или получать результат подзапроса без непосредственного выполнения всего запроса.
Скалярный подзапрос, дающий нулевые результаты, сам по себе будет равен null , тогда как подзапрос, содержащий несколько элементов, приведет к ошибке во время выполнения.
Когда скалярный подзапрос выдает только одно поле в результате, это поле становится результатом верхнего уровня для подзапроса. Чаще всего это наблюдается, когда подзапрос заканчивается оператором select(field("user_name")) или aggregate(countAll().as("total")) где схема подзапроса содержит только одно поле. В противном случае, если подзапрос может выдавать несколько полей, они заключаются в map.
Используйте обертку SDK toScalarExpression() для преобразования запроса в скалярное выражение.
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
});
}
Ответ
{
"id": "author_202",
"name": "Charles Dickens",
"averageBookRating": 4.65
}
Быстрый
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 ])
Ответ
{ "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) }
Ответ
{ "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); }
Ответ
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
subcollection(...) Подзапросы
Хотя входной этап subcollection(...) предлагается в качестве промежуточного звена, он позволяет выполнять объединения в иерархической модели данных Cloud Firestore . В иерархической модели запросам часто необходимо получать документ вместе с данными из его собственных подколлекций. Хотя этого можно добиться, используя входной этап collection_group(...) с последующим фильтром по родительской ссылке, subcollection(...) предоставляет гораздо более лаконичный синтаксис.
За исключением неявного условия объединения, этот запрос работает аналогично подзапросу к массиву, возвращая пустой результат, если ни один документ не найден, даже если вложенная коллекция не существует.
По сути, это синтаксический сахар : он автоматически использует __name__ в качестве ключа объединения для разрешения иерархической связи. Это делает его предпочтительным способом поиска в коллекциях, связанных отношением «родитель-потомок».
Передовые методы
- Управление памятью с помощью
toArrayExpression(): Будьте осторожны с подзапросамиtoArrayExpression(), поскольку материализация большого количества документов может исчерпать лимит памяти запроса (128 МиБ). Чтобы этого избежать, используйтеselect(...)внутри подзапроса, чтобы возвращать только необходимые поля, и применяйте фильтрыwhere(...)чтобы ограничить количество возвращаемых документов. При необходимости рассмотрите возможность использованияlimit(...)чтобы ограничить количество документов, возвращаемых подзапросом. - Индексирование: Убедитесь, что поля, используемые в предложении
where(...)подзапроса, проиндексированы. Эффективные соединения основаны на возможности выполнения поиска по индексу, а не на полном сканировании таблицы.
Для получения дополнительной информации о передовых методах работы с запросами обратитесь к нашему руководству по оптимизации запросов .
Ограничения
- Область видимости
subcollection(...): Этап вводаsubcollection(...)поддерживается только внутри подзапросов, поскольку для разрешения иерархической связи и выполнения объединения требуется контекст родительского документа. - Глубина вложенности: подзапросы могут быть вложены на глубину до 20 уровней.
- Использование памяти: Ограничение в 128 МиБ на материализованные данные применяется ко всему запросу, включая все объединенные документы.