การดำเนินการหลาย queries
รวมหลาย queries เข้าเป็น query เดียว แบ่งปันสถานะระหว่างกัน และดำเนินการตามลำดับที่ร้องขอ
คำอธิบาย
การดำเนินการหลาย queries จะรวมหลาย queries เข้าเป็น query เดียว โดยรับประกันว่าจะถูกดำเนินการตามลำดับที่ร้องขอเดียวกัน ออเปอเรชันสามารถสื่อสารสถานะระหว่างกันผ่านตัวแปรไดนามิก ซึ่งถูกคำนวณเพียงครั้งเดียวแต่สามารถอ่านได้หลายครั้งตลอดทั้งเอกสาร
query SomeQuery {
id @export(as: "rootID")
}
query AnotherQuery
@depends(on: "SomeQuery")
{
_echo(value: $rootID )
}ฟีเจอร์นี้มีประโยชน์หลายประการ:
- ช่วยเพิ่มประสิทธิภาพ: แทนที่จะดำเนินการ query ต่อ GraphQL เซิร์ฟเวอร์ แล้วรอการตอบกลับ จากนั้นใช้ผลลัพธ์นั้นเพื่อดำเนินการ query อีกตัวหนึ่ง เราสามารถรวม queries เข้าด้วยกันเป็นหนึ่งเดียวและดำเนินการในคำขอเดียว จึงหลีกเลี่ยงความหน่วงจากการเชื่อมต่อ HTTP หลายครั้งได้
- ช่วยให้เราจัดการ GraphQL queries ของเราเป็นออเปอเรชันแบบอะตอมมิก (หรือหน่วยตรรกะ) ที่ขึ้นต่อกัน และสามารถดำเนินการแบบมีเงื่อนไขโดยอิงจากผลลัพธ์ของออเปอเรชันก่อนหน้าได้
การดำเนินการหลาย queries แตกต่างจากการทำ query batching ซึ่ง GraphQL เซิร์ฟเวอร์ก็ดำเนินการหลาย queries ในคำขอเดียวเช่นกัน แต่ queries เหล่านั้นเพียงถูกดำเนินการทีละตัวต่อกันอย่างเป็นอิสระจากกัน
ไดเรกทีฟที่ถูกเปิดใช้งาน
เมื่อเปิดใช้งานการดำเนินการหลาย queries ไดเรกทีฟต่อไปนี้จะพร้อมใช้งานใน 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 (ในออเปอเรชัน)
เมื่อเปิดใช้งานการดำเนินการหลาย queries ไดเรกทีฟ @include และ @skip ก็พร้อมใช้งานเป็นไดเรกทีฟออเปอเรชันด้วย และสามารถใช้เพื่อดำเนินการออเปอเรชันแบบมีเงื่อนไขหากเป็นไปตามเงื่อนไขบางอย่าง
ตัวอย่างเช่น ใน query นี้ ออเปอเรชัน 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 นี้:
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 ถูกนำมาใช้กับหลายฟิลด์ (โดยการเพิ่มพารามิเตอร์ affectAdditionalFieldsUnderPos ที่มาจากโมดูล Multi-Field Directives) ค่าที่ถูกตั้งบนตัวแปรไดนามิกจะเป็นดิกชันนารีของ { key: field alias, value: field 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 / ฟิลด์เดียว
ตัวแปรไดนามิกจะมีอาร์เรย์ที่มีค่าของฟิลด์จากเอนทิตีทั้งหมดที่ถูก query (จากฟิลด์ที่ครอบอยู่) โดยการส่งพารามิเตอร์ type: LIST
เมื่อรัน query นี้ (ซึ่งเอนทิตีที่ถูก 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 / ฟิลด์เดียว
ตัวแปรไดนามิกจะมีดิกชันนารี (ประเภท JSONObject) โดยมี ID จากเอนทิตีที่ถูก query เป็นคีย์ และค่าของฟิลด์เป็นค่า โดยการส่งพารามิเตอร์ type: DICTIONARY
query นี้:
query {
posts(filter: { ids: [1, 5] }) {
title @export(as: "postIDTitles", type: DICTIONARY)
}
}…เอ็กซ์พอร์ตตัวแปรไดนามิก $postIDTitles ด้วยค่า:
{
"1": "Hello world!",
"5": "Everything good?"
}ประเภท DICTIONARY / มัลติฟิลด์
ในการรวมกันนี้ เราเอ็กซ์พอร์ตดิกชันนารีของดิกชันนารี: { key: entity ID, value: { key: field alias, value: field 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=... พร้อมชื่อของออเปอเรชันที่จะดำเนินการ มิฉะนั้นออเปอเรชันสุดท้ายจะถูกดำเนินการ
ตัวอย่างเช่น เพื่อดำเนินการออเปอเรชัน GetPostsContainingString ใน Persisted Query ที่มีเอนด์พอยต์ /graphql-query/posts-with-user-name/ เราต้องเรียกใช้:
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 spec
ฟังก์ชันนี้ปัจจุบันยังไม่เป็นส่วนหนึ่งของ GraphQL spec แต่ได้มีการร้องขอแล้ว: