多重 Query 执行
将多个 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(操作指令):用于让操作(query或mutation)指定哪些其他操作必须在其之前执行@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,被执行的操作(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 将一个字段(或一组字段)的值导出到动态变量中,以便在另一个 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参数的值(SINGLE、LIST或DICTIONARY之一)- 指令是应用于单个字段,还是应用于多个字段(通过 Multi-Field Directives 模块)
6 种可能的输出如下:
SINGLE类型:- 单字段
- 多字段
LIST类型:- 单字段
- 多字段
DICTIONARY类型:- 单字段
- 多字段
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 为 1 和 5 的文章):
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 规范的一部分,但已有相关提案请求: