อธิบาย Nested Mutations
Mutations คือการดำเนินการที่สามารถเปลี่ยนแปลงข้อมูลใน GraphQL server ได้ เช่น การสร้างโพสต์ การอัปเดตชื่อผู้ใช้ การเพิ่มความคิดเห็นในโพสต์ หรือการดำเนินการอื่น ๆ
ใน GraphQL นั้น mutations จะถูกเปิดเผยเฉพาะใน type MutationRoot เท่านั้น ดังนี้:
type MutationRoot {
createPost(id: ID!, title: String!, content: String): Post!
updateUserName(userID: ID!, newName: String!): User!
addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}(GraphQL schema ในคู่มือนี้ใช้เพื่ออธิบายตัวอย่างเท่านั้น และแตกต่างจาก schema ที่ plugin ให้มา)
ด้วย schema นี้ การเปลี่ยนชื่อผู้ใช้สามารถทำได้ดังนี้:
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
}Mutations จะถูกเปิดเผยเฉพาะใน mutation root object type เพื่อบังคับให้ดำเนินการแบบ serial ตามที่ อธิบายไว้ใน GraphQL spec:
It is expected that the top level fields in a mutation operation perform side‐effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side‐effects.
คำว่า "serial execution" นั้นตรงข้ามกับ "parallel execution" ซึ่ง parallel execution เป็นพฤติกรรมที่แนะนำสำหรับการ resolve fields
ตัวอย่างเช่น ใน query ด้านล่าง ไม่สำคัญว่า GraphQL server จะ resolve field ใดก่อน (ไม่ว่าจะเป็น name หรือ email) และสามารถ resolve ทั้งสองแบบ parallel ได้:
query {
user(by: { id: 37 }) {
name
email
}
}อย่างไรก็ตาม Mutations เปลี่ยนแปลงข้อมูล ดังนั้นลำดับที่ fields ถูก resolve จึงมีความสำคัญ จึงต้องดำเนินการแบบ serial (มิฉะนั้นอาจเกิด race conditions ได้)
ตัวอย่างเช่น queries สองตัวด้านล่างจะให้ผลลัพธ์ที่แตกต่างกัน:
# Query 1: after execution, user name will be "John"
mutation {
updateUserName(userID: 37, newName: "Peter") {
name
}
updateUserName(userID: 37, newName: "John") {
name
}
}
# Query 2: after execution, user name will be "Peter"
mutation {
updateUserName(userID: 37, newName: "John") {
name
}
updateUserName(userID: 37, newName: "Peter") {
name
}
}ผลที่ตามมาของการเปิดเผย mutations ผ่าน MutationRoot เท่านั้น คือ type นี้จะมีขนาดใหญ่มาก โดยมี fields ที่ไม่มีความเกี่ยวข้องกันนอกจากต้องดำเนินการแบบ serial (ซึ่งเป็นเรื่องทางเทคนิค ไม่ใช่การตัดสินใจด้านการออกแบบ interface)
เหตุผลที่ต้องใช้ Nested Mutations
จาก mutations ข้างต้น มีเพียง createPost เท่านั้นที่อยู่ใน type MutationRoot อย่างแท้จริง เพราะมันสร้าง element ใหม่จากที่ว่าง แต่ updateUserName และ addCommentToPost นั้นสามารถมีการดำเนินการเทียบเท่าที่ใช้กับ entity ที่มีอยู่แล้วจาก type อื่นได้อย่างสมบูรณ์:
type User {
updateName(newName: String!): User!
}
type Post {
addComment(comment: String!, userID: ID): Comment!
}ด้วย schema นี้ การเปลี่ยนชื่อผู้ใช้สามารถทำได้ดังนี้:
mutation {
user(ID: 37) {
updateName(newName: "Peter") {
name
}
}
}ฟีเจอร์นี้เรียกว่า "nested mutations": การนำ mutation ไปใช้กับผลลัพธ์ของการดำเนินการอื่น ไม่ว่าจะเป็น query หรือ mutation
โปรดสังเกตว่าการใช้ nested mutations ทำให้ GraphQL schema สวยงามขึ้นอย่างไร:
- ในขณะที่การดำเนินการ
MutationRoot.updateUserNameต้องรับIDของผู้ใช้ แต่การดำเนินการเทียบเท่าUser.updateNameไม่จำเป็นต้องรับ เพราะมันถูกดำเนินการบน user entity อยู่แล้ว - ชื่อ field ถูกย่อจาก
updateUserNameเป็นupdateName
นอกจากนี้ GraphQL service ยังเรียบง่ายและเข้าใจง่ายขึ้น เนื่องจากเราสามารถนำทางระหว่าง entities ใน graph เพื่อแก้ไขข้อมูลในแบบเดียวกับการ query ข้อมูล
Nested mutations สามารถลึกลงได้หลายระดับ ตัวอย่างเช่น เราสามารถเพิ่มความคิดเห็นในโพสต์ที่สร้างใหม่ ทั้งหมดใน query เดียว:
mutation {
createPost(ID: 37, title: "Hello world!", content: "Just another post") {
id
addComment(comment: "Lovely post") {
id
}
}
}จากนี้ nested mutations ยังสามารถปรับปรุงประสิทธิภาพโดยลด round-trip latency จากการดำเนินการ queries หลายครั้งเพื่อ mutate หลาย elements ให้เหลือเพียงการดำเนินการ query เดียว
เหตุผลที่ Nested Mutations ไม่เป็นส่วนหนึ่งของ Spec
GraphQL spec ถูกออกแบบมาเพื่อรองรับการ implementation ของ GraphQL server ทุกภาษา อย่างไรก็ตาม แรงขับเคลื่อนหลักคือ JavaScript ผ่าน graphql-js ซึ่งเป็น reference implementation
กล่าวอีกนัยหนึ่ง ฟีเจอร์ใดก็ตามที่ไม่สามารถรองรับโดย graphql-js จะไม่เป็นส่วนหนึ่งของ specification
เนื่องจาก JavaScript รองรับ promises การ resolve fields แบบ parallel จึงเป็นไปได้ และ parallelism ได้กลายเป็นหนึ่งในหลักการพื้นฐานเมื่อออกแบบ graphql-js ครั้งแรก ดังที่ปรากฏใน DataLoader (ชั้นการดึงข้อมูล) ซึ่ง batching functions ส่งคืน JavaScript promises
ข้อดีของ parallel execution ด้านประสิทธิภาพมีมากมาย และ nested mutations ไม่สามารถทำงานร่วมกับ parallelism ได้ จึงได้ตัดสินใจว่าไม่คุ้มที่จะแลก parallel execution กับ nested mutations
Nested Mutations และประสิทธิภาพ
สำหรับ plugin Gato GraphQL นั้น fields จะถูก resolve แบบ serial เสมอ และลำดับที่ถูก resolve นั้นเป็นแบบ deterministic (ลักษณะนี้ไม่ส่งผลต่อประสิทธิภาพการ resolve query เพราะ server จะแปลง graph ใน query เป็น component model ก่อน ซึ่ง resolve ในเวลาเชิงเส้นที่เหมาะสมที่สุด)
ซึ่งหมายความว่า plugin สามารถรองรับ nested mutations ได้ โดยให้ประโยชน์ทั้งหมดและไม่ได้รับผลกระทบใด ๆ
GraphQL Spec
ฟังก์ชันนี้ยังไม่เป็นส่วนหนึ่งของ GraphQL spec ในปัจจุบัน แต่มีการร้องขอไว้ใน: