Выполнение нескольких queries одновременно
Несколько queries можно объединить и выполнить как одну операцию, повторно используя их состояние и данные.
Это отличается от query batching, при котором GraphQL-сервер также выполняет несколько queries в одном запросе, но эти queries просто выполняются одна за другой, независимо друг от друга.
Эта функция улучшает производительность. Вместо того чтобы выполнять queries независимо в разных запросах (когда мы сначала выполняем операцию на GraphQL-сервере, затем ждём ответа, а потом используем результат для выполнения другой операции), мы можем выполнять их вместе, избегая таким образом задержек от нескольких запросов.
Multiple Query Execution также позволяет лучше организовать наши GraphQL queries, разбивая их на логические единицы, которые зависят друг от друга и выполняются условно на основе результата предыдущей операции.
Как использовать выполнение нескольких queries
Предположим, мы хотим найти все посты, в которых упоминается имя авторизованного пользователя. Обычно для этого потребовалось бы два queries:
Сначала получаем name пользователя:
query GetLoggedInUserName {
me {
name
}
}...а затем, выполнив первый query, можем передать полученный name пользователя как переменную $search для поиска во втором query:
query GetPostsContainingString($search: String!) {
posts(filter: { search: $search }) {
id
title
}
}Multiple Query Execution упрощает этот процесс, позволяя получить все данные и выполнить всю необходимую логику в одном запросе:
query GetLoggedInUserName {
me {
name @export(as: "search")
}
}
query GetPostsContainingString @depends(on: "GetLoggedInUserName") {
posts(filter: { search: $search }) {
id
title
}
}Multiple Query Execution достигается с помощью следующих специальных директив:
@depends(директива операции): заставляет операцию (будь тоqueryилиmutation) указывать, какие другие операции должны быть выполнены перед ней@export(директива поля): экспортирует значение некоторого поля из одной операции, чтобы внедрить его как входные данные в некоторое поле другой операции@deferredExport(директива поля): аналогично@export, но предназначено для использования с Multi-Field Directives
Кроме того, директивы @include и @skip также доступны как директивы операций (обычно они являются только директивами полей), и их можно использовать для условного выполнения операции при соблюдении некоторого условия.
GraphQL-сервер создаст список операций для загрузки и выполнения, извлекая их из каждого @depends(on: ...), и будет экспортировать значения из любого поля, содержащего @export, как динамическую переменную (с именем, определённым в аргументе as) для использования в любой последующей операции.
Комбинируя эти директивы, мы можем разбить любую сложную функциональность на промежуточные шаги, чередуя операции query и mutation, добавлять их зависимости в требуемом порядке и выполнять их все в одном запросе, указав внешнюю операцию в ?operationName=... (в приведённом выше примере это будет ?operationName=GetPostsContainingString).
Определение операций для загрузки и выполнения с помощью @depends
Когда GraphQL-документ содержит несколько операций, мы указываем серверу, какую из них выполнить, с помощью параметра URL ?operationName=...; в противном случае будет выполнена последняя операция.
Начиная с этой начальной операции, сервер соберёт все операции для выполнения, которые определяются добавлением директивы depends(on: [...]), и выполнит их в соответствующем порядке, соблюдая зависимости.
Аргумент operations директивы принимает массив имён операций ([String]), или мы также можем указать единственное имя операции (String).
В этом query мы передаём ?operationName=Four, и выполняемые операции (будь то query или mutation) будут ["One", "Two", "Three", "Four"]:
mutation One {
# Do something ...
}
mutation Two {
# Do something ...
}
query Three @depends(on: ["One", "Two"]) {
# Do something ...
}
query Four @depends(on: "Three") {
# Do something ...
}Совместное использование данных между queries с помощью @export
Директива @export экспортирует значение поля (или набора полей) в динамическую переменную для использования в качестве входных данных в некотором поле другого query.
Например, в этом query мы экспортируем имя авторизованного пользователя и используем это значение для поиска постов, содержащих данную строку (обратите внимание, что переменная $loggedInUserName, поскольку она динамическая, не нуждается в определении в операции FindPosts):
query GetLoggedInUserName {
me {
name @export(as: "loggedInUserName")
}
}
query FindPosts @depends(on: "GetLoggedInUserName") {
posts(filter: { search: $loggedInUserName }) {
id
}
}Выходные форматы динамических переменных
@export может производить 6 различных результатов на основе комбинации:
- Значения аргумента
type(либоSINGLE,LIST, либоDICTIONARY) - Применяется ли директива к одному полю или к нескольким полям (через модуль Multi-Field Directives)
6 возможных результатов:
- Тип
SINGLE:- Одиночное поле
- Несколько полей
- Тип
LIST:- Одиночное поле
- Несколько полей
- Тип
DICTIONARY:- Одиночное поле
- Несколько полей
Тип SINGLE / Одиночное поле
Результатом является единственное значение при передаче параметра type: SINGLE (который установлен как значение по умолчанию).
В этом query:
query {
post(by: { id: 1 }) {
title @export(as: "postTitle", type: SINGLE)
}
}...динамическая переменная $postTitle будет иметь значение:
"Hello world!"Обратите внимание: если SINGLE применяется к массиву сущностей, то экспортируется значение последней сущности.
В этом query:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitle", type: SINGLE)
}
}...динамическая переменная $postTitle будет иметь значение поста с ID 5:
"Everything good?"Тип SINGLE / Несколько полей
Если @export применяется к нескольким полям (путём добавления параметра affectAdditionalFieldsUnderPos, предоставляемого модулем Multi-Field Directives), то значением, устанавливаемым в динамическую переменную, является словарь { key: псевдоним поля, value: значение поля } (типа JSONObject).
Этот query:
query {
post(by: { id: 1 }) {
title
content
@export(
as: "postData",
type: SINGLE,
affectAdditionalFieldsUnderPos: [1]
)
}
}...экспортирует динамическую переменную $postData со значением:
{
"title": "Hello world!",
"content": "Lorem ipsum."
}Тип LIST / Одиночное поле
Динамическая переменная будет содержать массив со значением поля из всех запрошенных сущностей (из охватывающего поля) при передаче параметра type: LIST.
При выполнении этого query (в котором запрошенными сущностями являются посты с ID 1 и 5):
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitles", type: LIST)
}
}...динамическая переменная $postTitles будет иметь значение:
[
"Hello world!",
"Everything good?"
]Тип LIST / Несколько полей
Мы получаем массив словарей (типа JSONObject), каждый из которых содержит значения полей, к которым применяется директива.
Этот query:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsData",
type: LIST,
affectAdditionalFieldsUnderPos: [1]
)
}
}...экспортирует динамическую переменную $postsData со значением:
[
{
"title": "Hello world!",
"content": "Lorem ipsum."
},
{
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
]Тип DICTIONARY / Одиночное поле
Динамическая переменная будет содержать словарь (типа JSONObject) с ID запрошенной сущности в качестве ключа и значением поля в качестве значения при передаче параметра type: DICTIONARY.
Этот query:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postIDTitles", type: DICTIONARY)
}
}...экспортирует динамическую переменную $postIDTitles со значением:
{
"1": "Hello world!",
"5": "Everything good?"
}Тип DICTIONARY / Несколько полей
В этой комбинации мы экспортируем словарь словарей: { key: ID сущности, value: { key: псевдоним поля, value: значение поля } } (используя тип JSONObject, который будет содержать записи типа JSONObject).
Этот query:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsIDProperties",
type: DICTIONARY,
affectAdditionalFieldsUnderPos: [1]
)
}
}...экспортирует динамическую переменную $postsIDProperties со значением:
{
"1": {
"title": "Hello world!",
"content": "Lorem ipsum."
},
"5": {
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
}Условное выполнение операций
Когда Multiple Query Execution включён, директивы @include и @skip также доступны как директивы операций, и их можно использовать для условного выполнения операции при соблюдении некоторого условия.
Например, в этом query операция CheckIfPostExists экспортирует динамическую переменную $postExists и только в том случае, если её значение равно true, будет выполнена мутация ExecuteOnlyIfPostExists:
query CheckIfPostExists($id: ID!) {
# Initialize the dynamic variable to `false`
postExists: _echo(value: false) @export(as: "postExists")
post(by: { id: $id }) {
# Found the Post => Set dynamic variable to `true`
postExists: _echo(value: true) @export(as: "postExists")
}
}
mutation ExecuteOnlyIfPostExists
@depends(on: "CheckIfPostExists")
@include(if: $postExists)
{
# Do something...
}Экспорт значений при итерации массива или JSON-объекта
@export учитывает кардинальность любой охватывающей мета-директивы.
В частности, когда @export вложена под мета-директиву, которая итерирует элементы массива или свойства JSON-объекта (то есть @underEachArrayItem и @underEachJSONObjectProperty), экспортируемое значение будет массивом.
Этот query:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...производит $contentAttributes со значением:
[
"List Block",
"Columns Block",
"Columns inside Columns (nested inner blocks)",
"Life is so rich",
"Life is so dynamic"
]В отличие от этого, тот же query, обращающийся к конкретному элементу массива вместо итерации по всем элементам (заменяя @underEachArrayItem на @underArrayItem(index: 0)), экспортирует единственное значение.
Этот query:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underArrayItem(index: 0)
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...производит $contentAttributes со значением:
"List Block"Порядок выполнения директив
Если перед @export есть другие директивы, экспортируемое значение будет отражать изменения, внесённые этими предшествующими директивами.
Например, в этом query в зависимости от того, выполняется ли @export до или после @strUpperCase, результат будет разным:
query One {
id
# First export "root", only then will be converted to "ROOT"
@export(as: "id")
@strUpperCase
again: id
# First convert to "ROOT" and then export this value
@strUpperCase
@export(as: "again")
}
query Two @depends(on: "One") {
mirrorID: _echo(value: $id)
mirrorAgain: _echo(value: $again)
}Результат:
{
"data": {
"id": "ROOT",
"again": "ROOT",
"mirrorID": "root",
"mirrorAgain": "ROOT"
}
}Multi-Field Directives
Когда функция Multi-Field Directives включена и мы экспортируем значение нескольких полей в словарь, используйте @deferredExport вместо @export, чтобы гарантировать выполнение всех директив всех задействованных полей перед экспортом значения поля.
Например, в этом query первое поле имеет применённую директиву @strUpperCase, а второе — @titleCase. При выполнении @deferredExport экспортируемое значение будет иметь эти директивы применёнными:
query One {
id @strUpperCase # Will be exported as "ROOT"
again: id @titleCase # Will be exported as "Root"
@deferredExport(as: "props", affectAdditionalFieldsUnderPos: [1])
}
query Two @depends(on: "One") {
mirrorProps: _echo(value: $props)
}Результат:
{
"data": {
"id": "ROOT",
"again": "Root",
"mirrorProps": {
"id": "ROOT",
"again": "Root"
}
}
}Спецификация GraphQL
Данная функциональность в настоящее время не является частью спецификации GraphQL, однако соответствующие запросы поданы: