멀티플 쿼리 실행
여러 쿼리를 하나의 쿼리로 결합하여 상태를 공유하면서 지정된 순서대로 실행합니다.
설명
멀티플 쿼리 실행은 여러 쿼리를 하나의 쿼리로 결합하여 지정된 순서대로 실행되도록 보장합니다. 오퍼레이션들은 다이나믹 변수를 통해 서로 상태를 주고받을 수 있으며, 다이나믹 변수는 한 번만 계산되지만 문서 전체에서 여러 번 읽을 수 있습니다.
query SomeQuery {
id @export(as: "rootID")
}
query AnotherQuery
@depends(on: "SomeQuery")
{
_echo(value: $rootID )
}이 기능에는 여러 가지 이점이 있습니다.
- 성능이 향상됩니다. GraphQL 서버에 쿼리를 실행한 후 응답을 기다리고, 그 결과로 다른 쿼리를 실행하는 대신, 쿼리들을 하나로 결합하여 단일 요청으로 실행할 수 있어 여러 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가 적용되어 있고, 두 번째 필드에는 @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인수의 값(SINGLE,LIST또는DICTIONARY중 하나)- 디렉티브가 단일 필드에 적용되는지, 아니면 여러 필드에 적용되는지(Multi-Field Directives 모듈을 통해)
6가지 가능한 출력은 다음과 같습니다.
SINGLE타입:- 단일 필드
- 멀티 필드
LIST타입:- 단일 필드
- 멀티 필드
DICTIONARY타입:- 단일 필드
- 멀티 필드
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 타입 / 멀티 필드
@export가 여러 필드에 적용된 경우(Multi-Field Directives 모듈에서 제공하는 파라미터 affectAdditionalFieldsUnderPos를 추가하여), 다이나믹 변수에 설정되는 값은 { key: 필드 별칭, value: 필드 값 } 형태의 딕셔너리(JSONObject 타입)입니다.
이 쿼리:
query {
post(by: { id: 1 }) {
title
content
@export(
as: "postData",
type: SINGLE,
affectAdditionalFieldsUnderPos: [1]
)
}
}…다이나믹 변수 $postData를 다음 값으로 내보냅니다.
{
"title": "Hello world!",
"content": "Lorem ipsum."
}LIST 타입 / 단일 필드
파라미터 type: LIST를 전달하면 다이나믹 변수는 쿼리된 모든 엔티티(포함하는 필드에서)의 필드 값이 담긴 배열을 갖게 됩니다.
이 쿼리를 실행할 때(쿼리된 엔티티가 ID 1과 5인 게시물인 경우):
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postTitles", type: LIST)
}
}…다이나믹 변수 $postTitles는 다음 값을 갖게 됩니다.
[
"Hello world!",
"Everything good?"
]LIST 타입 / 멀티 필드
디렉티브가 적용된 필드들의 값을 각각 포함하는 딕셔너리(JSONObject 타입)의 배열을 얻습니다.
이 쿼리:
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를 전달하면 다이나믹 변수는 쿼리된 엔티티의 ID를 키로, 필드 값을 값으로 하는 딕셔너리(JSONObject 타입)를 갖게 됩니다.
이 쿼리:
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 {
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), 내보내는 값은 배열이 됩니다.
이 쿼리:
{
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"
]반면, @underEachArrayItem을 @underArrayItem(index: 0)으로 교체하여 모든 항목을 순회하는 대신 배열의 특정 항목에 접근하는 동일한 쿼리는 단일 값을 내보냅니다.
이 쿼리:
{
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 앞에 다른 디렉티브가 있으면, 내보내는 값은 이전 디렉티브들의 수정 사항을 반영합니다.
예를 들어, 이 쿼리에서 @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"
}
}퍼시스티드 쿼리에서의 실행
GraphQL 쿼리에 퍼시스티드 쿼리 내에 여러 오퍼레이션이 포함된 경우, 실행할 오퍼레이션의 이름을 URL 파라미터 ?operationName=...로 전달하여 해당 엔드포인트를 호출할 수 있습니다. 지정하지 않으면 마지막 오퍼레이션이 실행됩니다.
예를 들어, 엔드포인트 /graphql-query/posts-with-user-name/인 퍼시스티드 쿼리에서 오퍼레이션 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 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
}제공된 경우 뮤테이션 실행 전에 조건부로 사용자 로그인하기:
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 명세의 일부가 아니지만, 요청된 사항입니다.