Configuring the schema
Configuring the schemaExecuting multiple queries concurrently [PRO]

Executing multiple queries concurrently [PRO]

Multiple queries can be combined together, and executed as a single operation, reusing their state and their data.

This is different from query batching, in which the GraphQL server also executes multiple queries in a single request, but those queries are merely executed one after the other, independently from each other.

This feature improves performance. Instead of executing queries independenty in different requests (so that we first execute an operation against the GraphQL server, then wait for its response, and then use that result to perform another operation), we can execute them together, thus avoiding the latency from the several requests.

Multiple Query Execution also allow us to better organize our GraphQL queries, splitting them into logical units that depend on each other, and that are conditionally executed based on the result from a previous operation.

How to use multiple query execution

Let's suppose we want to search all posts which mention the name of the logged-in user. Normally, we would need two queries to accomplish this:

We first retrieve the user's name:

query GetLoggedInUserName {
  me {
    name
  }
}

...and then, having executed the first query, we can pass the retrieved user's name as variable $search to perform the search in a second query:

query GetPostsContainingString($search: String!) {
  posts(filter: { search: $search }) {
    id
    title
  }
}

Multiple Query Execution simplifies this process, allowing us to retrieve all data and execute all required logic in a single request:

query GetLoggedInUserName {
  me {
    name @export(as: "search")
  }
}
 
query GetPostsContainingString @depends(on: "GetLoggedInUserName") {
  posts(filter: { search: $search }) {
    id
    title
  }
}

Multiple Query Execution is attained with the use of these special directives:

  • @depends (operation directive): have an operation (whether a query or mutation) indicate what other operations must be executed before
  • @export (field directive): export some field value from one operation, to inject it as an input to some field in another operation
  • @deferredExport (field directive): Similar to @export but to be used with Multi-Field Directives.

In addition, directives @include and @skip are also made available as operation directives (they are normally only field directives), and these can be used to conditionally execute an operation if it satisfies some condition.

The GraphQL server will create the list of operations to load and execute, retrieving them from each @depends(on: ...), and will export the values from any field containing @export as a dynamic variable (with name defined under argument as) to be input in any subsequent operation.

Combining these directives, we are able to split any complex functionality into intermediate steps, alternating query and mutation operations, add their dependencies in the required order, and execute them all in a single request by defining the outermost operation in ?operationName=... (in the example above, that will be ?operationName=GetPostsContainingString).

Defining the operations to load and execute via @depends

When the GraphQL document contains multiple operations, we indicate to the server which one to execute via URL param ?operationName=...; otherwise, the last operation will be executed.

Starting from this initial operation, the server will collect all operations to execute, which are defined by adding directive depends(on: [...]), and execute them in the corresponding order respecting the dependencies.

Directive arg operations receives an array of operation names ([String]), or we can also provide a single operation name (String).

In this query, we pass ?operationName=Four, and the executed operations (whether query or mutation) will be ["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 ...
}

Sharing data across queries via @export

Directive @export exports the value of a field (or set of fields) into a dynamic variable, to be used as input in some field from another query.

For instance, in this query we export the logged-in user's name, and use this value to search for posts containing this string (please notice that variable $loggedInUserName, because it is dynamic, does not need be defined in operation FindPosts):

query GetLoggedInUserName {
  me {
    name @export(as: $loggedInUserName)
  }
}
 
query FindPosts @depends(on: "GetLoggedInUserName") {
  posts(filter: { search: $loggedInUserName }) {
    id
  }
}

Dynamic variable outputs

@export can produce 6 different outputs, based on a combination of:

  • The value of the type argument (either SINGLE, LIST or DICTIONARY)
  • If the directive is applied to a single field, or to multiple fields (via the Multi-Field Directives module)

The 6 possible outputs then are:

  1. SINGLE type:
    1. Single field
    2. Multi-field
  2. LIST type:
    1. Single field
    2. Multi-field
  3. DICTIONARY type:
    1. Single field
    2. Multi-field

SINGLE type / Single field

The output is a single value when passing param type: SINGLE (which is set as the default value).

In this query:

query {
  post(by: { id: 1 }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...the dynamic variable $postTitle will have value:

"Hello world!"

Please notice that if SINGLE is applied over an array of entities, then the value for the last entity is the one that is exported.

In this query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...the dynamic variable $postTitle will have the value for post with ID 5:

"Everything good?"

SINGLE type / Multi-field

If @export is applied on several fields (by adding param affectAdditionalFieldsUnderPos provided by the Multi-Field Directives module), then the value that is set on the dynamic variable is a dictionary of { key: field alias, value: field value } (of type JSONObject).

This query:

query {
  post(by: { id: 1 }) {
    title
    content
      @export(
        as: "postData",
        type: SINGLE,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exports dynamic variable $postData with value:

{
  "title": "Hello world!",
  "content": "Lorem ipsum."
}

LIST type / Single field

The dynamic variable will contain an array with the field value from all the queried entities (from the enclosing field), by passing param type: LIST.

When running this query (in which queried entities are posts with ID 1 and 5):

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitles", type: LIST)
  }
}

...the dynamic variable $postTitles will have value:

[
  "Hello world!",
  "Everything good?"
]

LIST type / Multi-field

We obtain an array of dictionaries (of type JSONObject), each containing the values of the fields on which the directive is applied.

This query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsData",
        type: LIST,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exports dynamic variable $postsData with value:

[
  {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
]

DICTIONARY type / Single field

The dynamic variable will contain a dictionary (of type JSONObject) with the ID from the queries entity as key, and the field values as value, by passing param type: DICTIONARY.

This query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postIDTitles", type: DICTIONARY)
  }
}

...exports dynamic variable $postIDTitles with value:

{
  "1": "Hello world!",
  "5": "Everything good?"
}

DICTIONARY type / Multi-field

In this combination, we export a dictionary of dictionaries: { key: entity ID, value: { key: field alias, value: field value } } (using a type JSONObject that will contain entries of type JSONObject).

This query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsIDProperties",
        type: DICTIONARY,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...exports dynamic variable $postsIDProperties with value:

{
  "1":
  {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  "5":
  {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
}

Conditional execution of operations

When Multiple Query Execution is enabled, directives @include and @skip are also available as operation directives, and these can be use to conditionally execute an operation if it satisfies some condition.

For instance, in this query, operation CheckIfPostExists exports a dynamic variable $postExists and, only if its value is true, will mutation ExecuteOnlyIfPostExists be executed:

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...
}

Exporting values when iterating an array or JSON object

@export respects the cardinality from any encompassing meta-directive.

In particular, whenever @export is nested below a meta-directive that iterates on array items or JSON object properties (i.e. @underEachArrayItem and @underEachJSONObjectProperty), then the exported value will be an array.

This query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underEachArrayItem
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produces $contentAttributes with value:

[
  "List Block",
  "Columns Block",
  "Columns inside Columns (nested inner blocks)",
  "Life is so rich",
  "Life is so dynamic"
]

In contrast, the same query that accesses a specific item in the array instead of iterating over all of them (by replacing @underEachArrayItem with @underArrayItem(index: 0)) will export a single value.

This query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underArrayItem(index: 0)
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produces $contentAttributes with value:

"List Block"

Directive execution order

If there are other directives before @export, the exported value will reflect the modifications by those previous directives.

For instance, in this query, depending on @export taking place before or after @strUpperCase, the result will be different:

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

Producing:

{
  "data": {
    "id": "ROOT",
    "again": "ROOT",
    "mirrorID": "root",
    "mirrorAgain": "ROOT"
  }
}

Multi-Field Directives

When the Multi-Field Directives feature is enabled and we export the value of multiple fields into a dictionary, use @deferredExport instead of @export to guarantee that all directives from every involved fields have been executed before exporting the field's value.

For instance, in this query, the first field has directive @strUpperCase applied to it, and the second has @titleCase. When executing @deferredExport, the exported value will have these directives applied:

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

Producing:

{
  "data": {
    "id": "ROOT",
    "again": "Root",
    "mirrorProps": {
      "id": "ROOT",
      "again": "Root"
    }
  }
}

GraphQL spec

This functionality is currently not part of the GraphQL spec, but it has been requested: