配置 Schema
配置 Schema并发执行多个 Query

并发执行多个 Query

多个 Query 可以组合在一起,作为单一操作执行,并共享彼此的状态与数据。

这与 query batching(批量查询)不同。在 query batching 中,GraphQL 服务器同样会在单次请求中执行多个 Query,但这些 Query 只是依次独立执行,彼此之间没有关联。

此功能可提升性能。与其在多个请求中独立执行 Query(先对 GraphQL 服务器执行一个操作,等待响应,再用结果执行另一个操作),不如将它们合并执行,从而避免多次请求带来的延迟。

Multiple Query Execution 还允许我们更好地组织 GraphQL Query,将其拆分为相互依赖、并根据前一个操作的结果有条件地执行的逻辑单元。

如何使用多 Query 执行

假设我们想搜索所有提及当前登录用户名字的文章。通常,这需要两个 Query 才能完成。

首先获取用户的 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(字段指令):与 Multi-Field Directives 配合使用时,功能类似于 @export

此外,@include@skip 也可作为操作指令使用(通常它们只是字段指令),可用于在满足某个条件时有条件地执行操作。

GraphQL 服务器会从每个 @depends(on: ...) 中获取并创建需要加载和执行的操作列表,并将任何包含 @export 的字段的值作为动态变量(名称由参数 as 定义)导出,以供后续操作使用。

将这些指令组合使用,我们可以将任何复杂功能拆分为中间步骤,交替使用 querymutation 操作,按所需顺序添加依赖关系,并通过在 ?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 ...
}

通过 @export 在 Query 之间共享数据

指令 @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 参数的值(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."
  }
}

操作的条件执行

启用 Multiple Query Execution 后,指令 @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...
}

在迭代数组或 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 规范的一部分,但已有相关提案: