多 Query 执行
多 Query 执行多重 Query 执行

多重 Query 执行

Included in the “Power Extensions” bundle

将多个 Query 合并为单个 Query,在它们之间共享状态,并按请求的顺序执行。

说明

多重 Query 执行将多个 Query 合并为单个 Query,确保它们按请求的顺序执行。操作之间可以通过动态变量相互传递状态,动态变量只计算一次,但可以在文档中多次读取。

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

此功能具有以下几项优势:

  • 它提升了性能:不必向 GraphQL 服务器执行一个 Query、等待响应,再用结果执行另一个 Query,而是可以将多个 Query 合并为一个,在单次请求中执行,从而避免多次 HTTP 连接带来的延迟。
  • 它让我们能够将 GraphQL Query 管理为相互依赖的原子操作(或逻辑单元),并可根据上一个操作的结果有条件地执行。

多重 Query 执行与 Query 批处理不同:Query 批处理中,GraphQL 服务器也会在单次请求中执行多个 Query,但这些 Query 仅仅是依次独立执行的,彼此之间没有关联。

已启用的指令

启用多重 Query 执行后,以下指令将在 GraphQL schema 中可用:

  • @depends(操作指令):用于让操作(querymutation)指定哪些其他操作必须在其之前执行
  • @export(字段指令):用于将某个 Query 中字段的值导出为动态变量,以便作为另一个 Query 中某个字段或指令的输入
  • @exportFrom(字段指令):类似于 @export,但用于导出作用域动态变量的值(通过 @passOnwards(as: "...")@applyField(passOnwardsAs: "...") 传递)
  • @deferredExport(字段指令):类似于 @export,但与 Multi-Field Directives 配合使用

此外,指令 @include@skip 也以操作指令的形式变得可用(通常它们只是字段指令),可用于在满足某些条件时有条件地执行操作。

@depends

当 GraphQL 文档包含多个操作时,我们通过 URL 参数 ?operationName=... 告知服务器要执行哪个操作;否则将执行最后一个操作。

从这个初始操作出发,服务器将收集所有待执行的操作(通过添加指令 depends(on: [...]) 来定义),并按照依赖关系的对应顺序执行它们。

指令参数 operations 接收操作名称数组([String]),也可以提供单个操作名称(String)。

在此 Query 中,我们传入 ?operationName=Four,被执行的操作(querymutation)将为 ["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 将一个字段(或一组字段)的值导出到动态变量中,以便在另一个 Query 的某个字段或 Query 中作为输入使用。

例如,在此 Query 中,我们导出已登录用户的名称,并用该值搜索包含此字符串的文章(请注意,变量 $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: "...") 传递的作用域动态变量的值。

例如,在此 Query 中,我们使用 @applyField 修改数组中的元素,并将新值赋给作用域动态变量 $replaced。然后使用 @exportFrom 通过动态变量 $replacedList 使该值在全局可访问,以便从后续 Query 中获取。

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 功能启用且需要将多个字段的值导出到字典时,请使用 @deferredExport 代替 @export,以确保在导出字段值之前,每个相关字段的所有指令都已执行完毕。

例如,在此 Query 中,第一个字段应用了指令 @strUpperCase,第二个字段应用了 @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(在操作中使用)

启用多重 Query 执行后,指令 @include@skip 也可作为操作指令使用,可用于在满足某些条件时有条件地执行操作。

例如,在此 Query 中,操作 CheckIfPostExists 导出动态变量 $postExists,只有当其值为 true 时,mutation 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 参数的值(SINGLELISTDICTIONARY 之一)
  • 指令是应用于单个字段,还是应用于多个字段(通过 Multi-Field Directives 模块)

6 种可能的输出如下:

  1. SINGLE 类型:
    1. 单字段
    2. 多字段
  2. LIST 类型:
    1. 单字段
    2. 多字段
  3. DICTIONARY 类型:
    1. 单字段
    2. 多字段

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 应用于多个字段(通过添加 Multi-Field Directives 模块提供的参数 affectAdditionalFieldsUnderPos),则动态变量中设置的值为 { 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 为 15 的文章):

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 类型 / 单字段

传入参数 type: DICTIONARY 时,动态变量将包含一个字典(类型为 JSONObject),以被查询实体的 ID 作为键,字段值作为值。

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

迭代数组或 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"
  }
}

在 Persisted Queries 中执行

当 GraphQL query 在 Persisted Query 中包含多个操作时,我们可以通过传入 URL 参数 ?operationName=... 加上要执行的操作名称来调用对应的端点;否则将执行最后一个操作。

例如,要在端点为 /graphql-query/posts-with-user-name/ 的 Persisted Query 中执行操作 GetPostsContainingString,需要调用:

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

示例

从外部 API 端点导入内容:

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

获取文章数据,对其进行转换,然后重新存储:

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

若文章存在则更新,否则显示错误消息:

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
}

在执行 mutation 之前将用户登录,执行完毕后立即登出:

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
}

如果提供了凭据,则在执行 mutation 之前有条件地将用户登录:

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 规范

此功能目前不是 GraphQL 规范的一部分,但已有相关提案请求: