Ejecución de Múltiples Consultas
Combina múltiples consultas en una sola, compartiendo estado entre ellas y ejecutándolas en el orden solicitado.
Descripción
La ejecución de múltiples consultas combina las consultas en una sola, asegurándose de que se ejecutan en el mismo orden solicitado. Las operaciones pueden comunicar el estado entre sí mediante variables dinámicas, que se computan una sola vez pero pueden leerse múltiples veces a lo largo del documento.
query SomeQuery {
id @export(as: "rootID")
}
query AnotherQuery
@depends(on: "SomeQuery")
{
_echo(value: $rootID )
}Esta funcionalidad ofrece varios beneficios:
- Mejora el rendimiento: En lugar de ejecutar una consulta contra el servidor GraphQL, esperar su respuesta, y luego usar ese resultado para ejecutar otra consulta, podemos combinar las consultas en una sola y ejecutarlas en una única petición, evitando así la latencia de las múltiples conexiones HTTP.
- Nos permite gestionar nuestras consultas GraphQL como operaciones atómicas (o unidades lógicas) que dependen unas de otras, y que pueden ejecutarse condicionalmente en función del resultado de una operación anterior.
La ejecución de múltiples consultas es diferente del query batching, en el que el servidor GraphQL también ejecuta múltiples consultas en una única petición, pero esas consultas simplemente se ejecutan una tras otra, de forma independiente.
Directivas habilitadas
Cuando la Ejecución de Múltiples Consultas está habilitada, las siguientes directivas están disponibles en el esquema GraphQL:
@depends(directiva de operación): Para que una operación (ya seaqueryomutation) indique qué otras operaciones deben ejecutarse antes@export(directiva de campo): Para exportar el valor de algún campo de una consulta como variable dinámica, que se pasará como entrada a algún campo o directiva en otra consulta@exportFrom(directiva de campo): Similar a@exportpero para exportar el valor de una variable dinámica con ámbito (pasada mediante@passOnwards(as: "...")o@applyField(passOnwardsAs: "..."))@deferredExport(directiva de campo): Similar a@exportpero para usar con Multi-Field Directives
Además, las directivas @include y @skip también están disponibles como directivas de operación (normalmente son solo directivas de campo), y pueden usarse para ejecutar condicionalmente una operación si cumple alguna condición.
@depends
Cuando el documento GraphQL contiene múltiples operaciones, indicamos al servidor cuál ejecutar mediante el parámetro URL ?operationName=...; de lo contrario, se ejecutará la última operación.
Partiendo de esta operación inicial, el servidor recopilará todas las operaciones a ejecutar, que se definen añadiendo la directiva depends(on: [...]), y las ejecutará en el orden correspondiente respetando las dependencias.
El argumento operations de la directiva recibe un array de nombres de operación ([String]), o también podemos proporcionar un único nombre de operación (String).
En esta consulta pasamos ?operationName=Four, y las operaciones ejecutadas (ya sean query o mutation) serán ["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 ...
}@export
La directiva @export exporta el valor de un campo (o conjunto de campos) a una variable dinámica, para usarse como entrada en algún campo o consulta de otra consulta.
Por ejemplo, en esta consulta exportamos el nombre del usuario con sesión iniciada, y usamos este valor para buscar entradas que contengan esta cadena (observa que la variable $loggedInUserName, al ser dinámica, no necesita definirse en la operación FindPosts):
query GetLoggedInUserName {
me {
name @export(as: "loggedInUserName")
}
}
query FindPosts @depends(on: "GetLoggedInUserName") {
posts(filter: { search: $loggedInUserName }) {
id
}
}@exportFrom
Es similar a @export, pero en lugar de exportar el valor del campo, exporta el valor de una variable dinámica con ámbito, pasada mediante @passOnwards(as: "...") o @applyField(passOnwardsAs: "...").
Por ejemplo, en esta consulta usamos @applyField para modificar los elementos del array y asignar este nuevo valor a la variable dinámica con ámbito $replaced. Después, usamos @exportFrom para hacer ese valor globalmente accesible mediante la variable dinámica $replacedList, de modo que pueda obtenerse desde una consulta posterior.
query One {
originalList: _echo(value: ["Hello everyone", "How are you?"])
@underEachArrayItem(
passValueOnwardsAs: "value"
affectDirectivesUnderPos: [1, 2]
)
@applyField(
name: "_strReplace"
arguments: {
search: " "
replaceWith: "-"
in: $value
},
passOnwardsAs: "replaced"
)
@exportFrom(
scopedDynamicVariable: $replaced,
as: "replacedList"
)
}
query Two @depends(on: "One") {
transformedList: _echo(value: $replacedList)
}Esto producirá:
{
"data": {
"originalList": [
"Hello everyone",
"How are you?"
],
"transformedList": [
"Hello-everyone",
"How-are-you?"
]
}
}@deferredExport
Cuando la funcionalidad Multi-Field Directives está habilitada y exportamos el valor de múltiples campos en un diccionario, usa @deferredExport en lugar de @export para garantizar que todas las directivas de cada campo involucrado se hayan ejecutado antes de exportar el valor del campo.
Por ejemplo, en esta consulta, el primer campo tiene la directiva @strUpperCase aplicada, y el segundo tiene @strTitleCase. Al ejecutar @deferredExport, el valor exportado tendrá estas directivas aplicadas:
query One {
id @strUpperCase # Will be exported as "ROOT"
again: id @strTitleCase # Will be exported as "Root"
@deferredExport(as: "props", affectAdditionalFieldsUnderPos: [1])
}
query Two @depends(on: "One") {
mirrorProps: _echo(value: $props)
}Produciendo:
{
"data": {
"id": "ROOT",
"again": "Root",
"mirrorProps": {
"id": "ROOT",
"again": "Root"
}
}
}@skip y @include (en operaciones)
Cuando la Ejecución de Múltiples Consultas está habilitada, las directivas @include y @skip también están disponibles como directivas de operación, y pueden usarse para ejecutar condicionalmente una operación si cumple alguna condición.
Por ejemplo, en esta consulta, la operación CheckIfPostExists exporta una variable dinámica $postExists y, solo si su valor es true, la mutación ExecuteOnlyIfPostExists se ejecutará:
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...
}Salidas de variables dinámicas
@export puede producir 6 salidas diferentes, basadas en una combinación de:
- El valor del argumento
type(ya seaSINGLE,LISToDICTIONARY) - Si la directiva se aplica a un único campo, o a múltiples campos (mediante el módulo Multi-Field Directives)
Las 6 salidas posibles entonces son:
- Tipo
SINGLE:- Campo único
- Multi-campo
- Tipo
LIST:- Campo único
- Multi-campo
- Tipo
DICTIONARY:- Campo único
- Multi-campo
Tipo SINGLE / Campo único
La salida es un único valor cuando se pasa el parámetro type: SINGLE (que está establecido como valor por defecto).
En esta consulta:
query {
post(by: { id: 1 }) {
title @export(as: "postTitle", type: SINGLE)
}
}...la variable dinámica $postTitle tendrá el valor:
"Hello world!"Ten en cuenta que si SINGLE se aplica sobre un array de entidades, entonces el valor de la última entidad es el que se exporta.
En esta consulta:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitle", type: SINGLE)
}
}...la variable dinámica $postTitle tendrá el valor de la entrada con ID 5:
"Everything good?"Tipo SINGLE / Multi-campo
Si @export se aplica sobre varios campos (añadiendo el parámetro affectAdditionalFieldsUnderPos proporcionado por el módulo Multi-Field Directives), entonces el valor que se establece en la variable dinámica es un diccionario de { key: field alias, value: field value } (de tipo JSONObject).
Esta consulta:
query {
post(by: { id: 1 }) {
title
content
@export(
as: "postData",
type: SINGLE,
affectAdditionalFieldsUnderPos: [1]
)
}
}...exporta la variable dinámica $postData con el valor:
{
"title": "Hello world!",
"content": "Lorem ipsum."
}Tipo LIST / Campo único
La variable dinámica contendrá un array con el valor del campo de todas las entidades consultadas (del campo contenedor), pasando el parámetro type: LIST.
Al ejecutar esta consulta (en la que las entidades consultadas son entradas con ID 1 y 5):
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitles", type: LIST)
}
}...la variable dinámica $postTitles tendrá el valor:
[
"Hello world!",
"Everything good?"
]Tipo LIST / Multi-campo
Obtenemos un array de diccionarios (de tipo JSONObject), cada uno conteniendo los valores de los campos sobre los que se aplica la directiva.
Esta consulta:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsData",
type: LIST,
affectAdditionalFieldsUnderPos: [1]
)
}
}...exporta la variable dinámica $postsData con el valor:
[
{
"title": "Hello world!",
"content": "Lorem ipsum."
},
{
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
]Tipo DICTIONARY / Campo único
La variable dinámica contendrá un diccionario (de tipo JSONObject) con el ID de la entidad consultada como clave, y los valores del campo como valor, pasando el parámetro type: DICTIONARY.
Esta consulta:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postIDTitles", type: DICTIONARY)
}
}...exporta la variable dinámica $postIDTitles con el valor:
{
"1": "Hello world!",
"5": "Everything good?"
}Tipo DICTIONARY / Multi-campo
En esta combinación, exportamos un diccionario de diccionarios: { key: entity ID, value: { key: field alias, value: field value } } (usando un tipo JSONObject que contendrá entradas de tipo JSONObject).
Esta consulta:
query {
posts(filter: { ids: [1, 5] }) {
title
content
@export(
as: "postsIDProperties",
type: DICTIONARY,
affectAdditionalFieldsUnderPos: [1]
)
}
}...exporta la variable dinámica $postsIDProperties con el valor:
{
"1": {
"title": "Hello world!",
"content": "Lorem ipsum."
},
"5": {
"title": "Everything good?",
"content": "Quisque convallis libero in sapien pharetra tincidunt."
}
}Exportando valores al iterar un array u objeto JSON
@export respeta la cardinalidad de cualquier meta-directiva que la envuelva.
En particular, siempre que @export esté anidada bajo una meta-directiva que itere sobre elementos de array o propiedades de objeto JSON (es decir, @underEachArrayItem y @underEachJSONObjectProperty), entonces el valor exportado será un array.
Esta consulta:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underEachArrayItem
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...produce $contentAttributes con el valor:
[
"List Block",
"Columns Block",
"Columns inside Columns (nested inner blocks)",
"Life is so rich",
"Life is so dynamic"
]En contraste, la misma consulta que accede a un elemento específico del array en lugar de iterar sobre todos ellos (reemplazando @underEachArrayItem por @underArrayItem(index: 0)) exportará un único valor.
Esta consulta:
{
post(by: { id: 19 }) {
coreContentAttributeBlocks: blockFlattenedDataItems(
filterBy: { include: "core/heading" }
)
@underArrayItem(index: 0)
@underJSONObjectProperty(
by: { path: "attributes.content" },
)
@export(
as: "contentAttributes",
)
}
}...produce $contentAttributes con el valor:
"List Block"Orden de ejecución de las directivas
Si hay otras directivas antes de @export, el valor exportado reflejará las modificaciones realizadas por esas directivas previas.
Por ejemplo, en esta consulta, dependiendo de si @export tiene lugar antes o después de @strUpperCase, el resultado será diferente:
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)
}Produciendo:
{
"data": {
"id": "ROOT",
"again": "ROOT",
"mirrorID": "root",
"mirrorAgain": "ROOT"
}
}Ejecución en Persisted Queries
Cuando una consulta GraphQL contiene múltiples operaciones en una Persisted Query, podemos invocar el endpoint correspondiente pasando el parámetro URL ?operationName=... con el nombre de la operación a ejecutar; de lo contrario, se ejecutará la última operación.
Por ejemplo, para ejecutar la operación GetPostsContainingString en una Persisted Query con endpoint /graphql-query/posts-with-user-name/, debemos invocar:
https://mysite.com/graphql-query/posts-with-user-name/?operationName=GetPostsContainingStringEjemplos
Importar contenido desde un endpoint de API externo:
query FetchDataFromExternalEndpoint
{
_sendJSONObjectItemHTTPRequest(input: { url: "https://site.com/wp-json/wp/posts/1" } )
@export(as: "externalData")
@remove
}
query ManipulateDataIntoInput @depends(on: "FetchDataFromExternalEndpoint")
{
title: _objectProperty(
object: $externalData,
by: {
path: "title.rendered"
}
) @export(as: "postTitle")
excerpt: _objectProperty(
object: $externalData,
by: {
key: "excerpt"
}
) @export(as: "postExcerpt")
}
mutation CreatePost @depends(on: "ManipulateDataIntoInput")
{
createPost(input: {
title: $postTitle
excerpt: $postExcerpt
}) {
id
}
}Obtener los datos de una entrada, transformarlos y almacenarlos de nuevo:
query GetPostData(
$postId: ID!
) {
post(by: {id: $postId}) {
id
title @export(as: "postTitle")
rawContent @export(as: "postContent")
}
}
query AdaptPostData(
$replaceFrom: String!,
$replaceTo: String!
)
@depends(on: "GetPostData")
{
adaptedPostTitle: _strReplace(
search: $replaceFrom
replaceWith: $replaceTo
in: $postTitle
)
@export(as: "adaptedPostTitle")
adaptedPostContent: _strReplace(
search: $replaceFrom
replaceWith: $replaceTo
in: $postContent
)
@export(as: "adaptedPostContent")
}
mutation StoreAdaptedPostData(
$postId: ID!
)
@depends(on: "AdaptPostData")
{
updatePost(input: {
id: $postId,
title: $adaptedPostTitle,
contentAs: { html: $adaptedPostContent },
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
id
title
rawContent
}
}
}Actualizar una entrada si existe, o mostrar un mensaje de error en caso contrario:
query GetPost($id: ID!) {
post(by:{id: $id}) {
id
title
}
_notNull(value: $__post) @export(as: "postExists")
}
query FailIfPostNotExists($id: ID!)
@skip(if: $postExists)
@depends(on: "GetPost")
{
errorMessage: _sprintf(
string: "There is no post with ID '%s'",
values: [$id]
) @remove
_fail(
message: $__errorMessage
data: {
id: $id
}
) @remove
}
mutation UpdatePost($id: ID!, $postTitle: String)
@include(if: $postExists)
@depends(on: "GetPost")
{
updatePost(input: {
id: $id,
title: $postTitle,
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
post {
id
title
rawContent
}
}
}
query MaybeUpdatePost
@depends(on: [
"FailIfPostNotExists",
"UpdatePost"
])
{
id @remove
}Iniciar sesión del usuario antes de ejecutar una mutación, y cerrar sesión inmediatamente después:
mutation LogUserIn(
$username: String!
$password: String!
) {
loginUser(by: {
credentials: {
usernameOrEmail: $username,
password: $password
}
}) @remove {
status
user {
id
username
}
}
}
mutation AddComment(
$customPostId: ID!
$commentContent: HTML!
)
@depends(on: "LogUserIn")
{
addCommentToCustomPost(input: {
customPostID: $customPostId,
commentAs: { html: $commentContent }
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
comment {
id
parent {
id
}
content
date
author {
name
email
}
}
}
}
mutation LogUserOut
@depends(on: "AddComment")
{
logoutUser @remove {
status
userID
}
}
query ExecuteAllAddCommentOperations
@depends(on: "LogUserOut")
{
id @remove
}Iniciar sesión condicionalmente del usuario antes de ejecutar una mutación, si se proporciona:
query ExportUserLogin(
$username: String
) {
_notNull(value: $username)
@export(as: "hasUsername")
@remove
}
mutation MaybeLogUserIn(
$username: String
$password: String
)
@depends(on: "ExportUserLogin")
@include(if: $hasUsername)
{
loginUser(by: {
credentials: {
usernameOrEmail: $username,
password: $password
}
}) @remove {
status
user {
id
username
}
}
}
mutation AddComment(
$customPostId: ID!
$commentContent: HTML!
)
@depends(on: "MaybeLogUserIn")
{
addCommentToCustomPost(input: {
customPostID: $customPostId,
commentAs: { html: $commentContent }
}) {
status
errors {
__typename
...on ErrorPayload {
message
}
}
comment {
id
parent {
id
}
content
date
author {
name
email
}
}
}
}
mutation MaybeLogUserOut
@depends(on: "AddComment")
@include(if: $hasUsername)
{
logoutUser @remove {
status
userID
}
}
query ExecuteAllAddCommentOperations
@depends(on: "MaybeLogUserOut")
{
id @remove
}Especificación GraphQL
Esta funcionalidad actualmente no forma parte de la especificación de GraphQL, pero ha sido solicitada: