Multiple Query Execution
Multiple Query Executionマルチプルクエリ実行

マルチプルクエリ実行

Included in the “Power Extensions” bundle

複数のクエリを1つのクエリにまとめ、それらの間で状態を共有しながら、指定された順序で実行します。

説明

マルチプルクエリ実行は複数のクエリを1つのクエリにまとめ、指定された順序で実行されることを保証します。オペレーション同士はダイナミック変数を通じて状態をやり取りすることができ、ダイナミック変数は1度だけ計算されますが、ドキュメント全体を通じて複数回読み取ることができます。

query SomeQuery {
  id @export(as: "rootID")
}
 
query AnotherQuery
  @depends(on: "SomeQuery")
{
  _echo(value: $rootID )
}

この機能にはいくつかのメリットがあります。

  • パフォーマンスが向上します。GraphQL サーバーにクエリを実行し、レスポンスを待ってから、その結果を使って別のクエリを実行するという手順の代わりに、複数のクエリを1つにまとめて単一のリクエストで実行できるため、複数の HTTP 接続による遅延を回避できます。
  • GraphQL クエリを、互いに依存し合う原子的なオペレーション(または論理的な単位)として管理し、前のオペレーションの結果に基づいて条件付きで実行できるようになります。

マルチプルクエリ実行はクエリバッチングとは異なります。クエリバッチングでも GraphQL サーバーが単一のリクエストで複数のクエリを実行しますが、それらのクエリは単に互いに独立して順番に実行されるだけです。

有効化されるディレクティブ

マルチプルクエリ実行が有効な場合、次のディレクティブが GraphQL スキーマで利用できるようになります。

  • @depends(オペレーションディレクティブ): オペレーション(query または mutation)が、事前に実行しなければならない他のオペレーションを指定するために使用します
  • @export(フィールドディレクティブ): あるクエリからフィールドの値をダイナミック変数としてエクスポートし、別のクエリのフィールドまたはディレクティブへの入力として使用するために使用します
  • @exportFrom(フィールドディレクティブ): @export に似ていますが、スコープ付きダイナミック変数(@passOnwards(as: "...") または @applyField(passOnwardsAs: "...") を通じて渡される)の値をエクスポートするために使用します
  • @deferredExport(フィールドディレクティブ): @export に似ていますが、Multi-Field Directives と組み合わせて使用します

また、ディレクティブ @include@skip もオペレーションディレクティブとして利用できるようになります(通常はフィールドディレクティブのみですが)。これらを使用して、何らかの条件を満たす場合にオペレーションを条件付きで実行できます。

@depends

GraphQL ドキュメントに複数のオペレーションが含まれる場合、URL パラメーター ?operationName=... を使用してサーバーに実行するオペレーションを指定します。指定しない場合は、最後のオペレーションが実行されます。

この最初のオペレーションを起点に、サーバーはディレクティブ depends(on: [...]) を追加することで定義されたすべての実行対象オペレーションを収集し、依存関係を尊重した対応する順序で実行します。

ディレクティブ引数 operations はオペレーション名の配列([String])を受け取ります。また、単一のオペレーション名(String)を指定することもできます。

このクエリでは ?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 ...
}

@export

ディレクティブ @export はフィールド(またはフィールドのセット)の値をダイナミック変数にエクスポートし、別のクエリのフィールドやクエリへの入力として使用できるようにします。

例えば、このクエリではログイン中のユーザー名をエクスポートし、その値を使ってこの文字列を含む投稿を検索します(変数 $loggedInUserName はダイナミック変数であるため、オペレーション FindPosts で定義する必要がないことに注意してください)。

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

@exportFrom

@export に似ていますが、フィールドの値をエクスポートするのではなく、@passOnwards(as: "...") または @applyField(passOnwardsAs: "...") を通じて渡されたスコープ付きダイナミック変数の値をエクスポートします。

例えば、このクエリでは @applyField を使用して配列の要素を変更し、この新しい値をスコープ付きダイナミック変数 $replaced に割り当てます。次に @exportFrom を使用して、その値をダイナミック変数 $replacedList を通じてグローバルにアクセス可能にし、後続のクエリから取得できるようにします。

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

これにより次の結果が生成されます。

{
  "data": {
    "originalList": [
      "Hello everyone",
      "How are you?"
    ],
    "transformedList": [
      "Hello-everyone",
      "How-are-you?"
    ]
  }
}

@deferredExport

Multi-Field Directives 機能が有効な場合で、複数のフィールドの値をディクショナリにエクスポートするときは、フィールドの値をエクスポートする前に各関連フィールドのすべてのディレクティブが実行済みであることを保証するために、@export の代わりに @deferredExport を使用してください。

例えば、このクエリでは、最初のフィールドにディレクティブ @strUpperCase が適用され、2番目のフィールドには @strTitleCase が適用されています。@deferredExport を実行すると、エクスポートされた値にこれらのディレクティブが適用されます。

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

結果:

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

@skip@include(オペレーションでの使用)

マルチプルクエリ実行が有効な場合、ディレクティブ @include@skip もオペレーションディレクティブとして利用できるようになります。これらを使用して、何らかの条件を満たす場合にオペレーションを条件付きで実行できます。

例えば、このクエリでは、オペレーション 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...
}

ダイナミック変数の出力

@export は次の組み合わせに基づいて6種類の異なる出力を生成できます。

  • type 引数の値(SINGLELIST、または DICTIONARY のいずれか)
  • ディレクティブが単一フィールドに適用されるか、複数のフィールドに適用されるか(Multi-Field Directives モジュールを使用)

6つの可能な出力は次のとおりです。

  1. SINGLE タイプ:
    1. 単一フィールド
    2. マルチフィールド
  2. LIST タイプ:
    1. 単一フィールド
    2. マルチフィールド
  3. DICTIONARY タイプ:
    1. 単一フィールド
    2. マルチフィールド

SINGLE タイプ / 単一フィールド

パラメーター type: SINGLE(デフォルト値として設定されています)を渡すと、出力は単一の値になります。

このクエリでは:

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

…ダイナミック変数 $postTitle は次の値を持ちます。

"Hello world!"

SINGLE をエンティティの配列に適用した場合、エクスポートされるのは最後のエンティティの値であることに注意してください。

このクエリでは:

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

…ダイナミック変数 $postTitle は 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."
  }
}

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

結果:

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

Execution in Persisted Queries

When a GraphQL query contains multiple operations in a Persisted Query, we can invoke the corresponding endpoint passing URL param ?operationName=... with the name of the operation to execute; otherwise, the last operation will be executed.

For instance, to execute operation GetPostsContainingString in a Persisted Query with endpoint /graphql-query/posts-with-user-name/, we must invoke:

https://mysite.com/graphql-query/posts-with-user-name/?operationName=GetPostsContainingString

Examples

Import content from an external API endpoint:

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

Retrieve the data for a post, transform it, and store it again:

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

Update a post if it exists, or show an error message otherwise:

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
}

Log the user in before executing a mutation, and out immediately after:

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
}

Conditionally log the user in before executing a mutation, if provided:

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
}

GraphQL spec

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