การพัฒนาสกีมาด้วยการกำหนดเวอร์ชันของฟิลด์
เมื่อความต้องการของแอปพลิเคชันพัฒนาขึ้น GraphQL API ที่จัดหาข้อมูลให้แอปพลิเคชันก็จำเป็นต้องพัฒนาตามไปด้วย โดยนำการเปลี่ยนแปลงมาสู่สกีมา เมื่อการเปลี่ยนแปลงเป็นแบบไม่ทำให้เกิดความเสียหาย เช่น การเพิ่มประเภทหรือฟิลด์ใหม่ เราสามารถนำไปใช้งานได้โดยตรงโดยไม่ต้องกังวลเรื่องผลข้างเคียง แต่เมื่อการเปลี่ยนแปลงเป็นแบบที่ทำให้เกิดความเสียหาย เราต้องตรวจสอบให้แน่ใจว่าเราไม่ได้นำบั๊กหรือพฤติกรรมที่ไม่คาดคิดเข้ามาในแอปพลิเคชัน
การเปลี่ยนแปลงที่ทำให้เกิดความเสียหาย ได้แก่ การลบประเภท ฟิลด์ หรือ directive หรือการแก้ไข signature ของฟิลด์ (หรือ directive) ที่มีอยู่แล้ว เช่น:
- การเปลี่ยนชื่อฟิลด์
- การเปลี่ยนประเภทของ argument ของฟิลด์ที่มีอยู่ หรือทำให้บังคับกรอก
- การเพิ่ม argument บังคับใหม่ให้กับฟิลด์
- การเพิ่ม non-nullable ให้กับประเภทการตอบสนองของฟิลด์
เพื่อจัดการกับการเปลี่ยนแปลงที่ทำให้เกิดความเสียหาย มีกลยุทธ์หลักสองประการ: การกำหนดเวอร์ชันและการพัฒนา ซึ่ง REST และ GraphQL ใช้งานตามลำดับ
REST API ระบุเวอร์ชันของ API ที่จะใช้ ไม่ว่าจะบน URL ของ endpoint (เช่น https://api.mycompany.com/v1 หรือ https://api-v1.mycompany.com) หรือผ่าน header บางอย่าง (เช่น Accept-version: v1) ผ่านการกำหนดเวอร์ชัน การเปลี่ยนแปลงที่ทำให้เกิดความเสียหายจะถูกเพิ่มเข้าไปในเวอร์ชันใหม่ของ API และเนื่องจากไคลเอนต์ต้องระบุเวอร์ชันใหม่ของ API อย่างชัดเจน พวกเขาจะรับรู้ถึงการเปลี่ยนแปลง
GraphQL ไม่ได้ปฏิเสธการใช้การกำหนดเวอร์ชัน แต่ส่งเสริมการใช้การพัฒนา ดังที่ระบุไว้ในหน้า GraphQL best practices:
While there's nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.
การพัฒนาทำงานต่างออกไปตรงที่ไม่ได้เกิดขึ้นทุกไม่กี่เดือนอย่างการกำหนดเวอร์ชัน แต่เป็นกระบวนการต่อเนื่อง แม้แต่รายวันหากจำเป็น ซึ่งเหมาะสมกับการวนซ้ำอย่างรวดเร็ว แนวทางนี้ถูกกำหนดโดย Principled GraphQL ซึ่งเป็นชุดแนวปฏิบัติที่ดีที่สุดสำหรับการพัฒนาบริการ GraphQL ในหลักการที่ห้า:
5. Use an Agile Approach to Schema Development: The schema should be built incrementally based on actual requirements and evolve smoothly over time
การพัฒนาสกีมา
ผ่านการพัฒนา ฟิลด์ที่มีการเปลี่ยนแปลงที่ทำให้เกิดความเสียหายต้องผ่านกระบวนการดังต่อไปนี้:
- นำฟิลด์กลับมาใช้งานใหม่โดยใช้ชื่อที่แตกต่างออกไป
- ประกาศให้ฟิลด์เป็น deprecated โดยขอให้ไคลเอนต์ใช้ฟิลด์ใหม่แทน
- เมื่อฟิลด์ไม่ถูกใช้งานโดยใครอีกต่อไป ให้ลบออกจากสกีมา
ลองดูตัวอย่าง สมมติว่าเรามีประเภท Account ที่จำลองบัญชีให้เป็นบุคคลที่มีชื่อและนามสกุลผ่านสกีมานี้ (ใช้ SDL ของ GraphQL - Schema Definition Language):
type Account {
id: Int
name: String!
surname: String!
}ในสกีมานี้ ทั้งฟิลด์ name และ surname เป็นฟิลด์บังคับ (นั่นคือสัญลักษณ์ ! ที่เพิ่มหลังประเภท String) เนื่องจากเราคาดว่าทุกคนจะมีทั้งชื่อและนามสกุล
ในที่สุด เราก็อนุญาตให้องค์กรเปิดบัญชีได้ด้วย แต่องค์กรไม่มีนามสกุล ดังนั้นเราต้องเปลี่ยน signature ของฟิลด์ surname เพื่อทำให้ไม่บังคับ:
type Account {
id: Int
name: String!
surname: String # This has changed
}นี่เป็นการเปลี่ยนแปลงที่ทำให้เกิดความเสียหาย เพราะแอปพลิเคชันไม่ได้คาดว่าฟิลด์ surname จะคืนค่า null ดังนั้นอาจไม่ได้ตรวจสอบเงื่อนไขนี้ เช่น เมื่อรัน JavaScript code นี้:
// This will fail when account.surname is null
const upperCaseSurname = account.surname.toUpperCase();บั๊กที่อาจเกิดขึ้นจากการเปลี่ยนแปลงที่ทำให้เกิดความเสียหายสามารถหลีกเลี่ยงได้โดยการพัฒนาสกีมา:
- เราไม่แก้ไข signature ของฟิลด์
surnameแต่ทำเครื่องหมายว่าเป็น deprecated พร้อมเพิ่มข้อความที่เป็นประโยชน์ระบุชื่อของฟิลด์ที่มาแทนที่ - เราแนะนำชื่อฟิลด์ใหม่
personSurname(หรือaccountSurname) เข้าสู่สกีมา
ประเภท Account ของเราจะมีลักษณะดังนี้:
type Account {
id: Int
name: String!
surname: String! @deprecated(reason: "Use `personSurname`")
personSurname: String
}สุดท้าย โดยการรวบรวม log ของ queries จากไคลเอนต์ เราสามารถวิเคราะห์ว่าพวกเขาได้เปลี่ยนมาใช้ฟิลด์ใหม่หรือยัง เมื่อใดก็ตามที่เราสังเกตว่าฟิลด์ surname ไม่ถูกใช้โดยใครอีกต่อไป เราสามารถลบออกจากสกีมาได้:
type Account {
id: Int
name: String!
personSurname: String
}ปัญหาของการพัฒนา
ตัวอย่างที่อธิบายข้างต้นเรียบง่ายมาก แต่ก็แสดงให้เห็นปัญหาที่อาจเกิดขึ้นบางประการจากการพัฒนาสกีมา:
| ปัญหา | คำอธิบาย |
|---|---|
| ชื่อฟิลด์กลายเป็นสิ่งที่ไม่สวยงาม | ในครั้งแรกที่เราตั้งชื่อฟิลด์ เราอาจพบชื่อที่เหมาะสมที่สุด เช่น surname แต่เมื่อเราต้องแทนที่มัน เราต้องสร้างชื่อที่แตกต่างออกไปซึ่งอาจไม่ดีเท่า (ชื่อที่ดีที่สุดถูกใช้ไปแล้ว!) ตัวเลือกทั้งหมดในตัวอย่างข้างต้นมีปัญหา:- personName ระบุอย่างชัดเจนว่าบัญชีนั้นสำหรับบุคคล ดังนั้นถ้าในภายหลังเราต้องเปิดบัญชีสำหรับผู้ที่ไม่ใช่บุคคลที่มีนามสกุล (ใครจะรู้... มนุษย์ดาวอังคาร?) เราก็ต้องพัฒนาสกีมาอีกครั้งเพื่อรักษาชื่อให้สอดคล้องกัน- ส่วน "account" ใน accountName ซ้ำซ้อนอย่างสมบูรณ์เนื่องจากประเภทคือ Account อยู่แล้ว- ไม่งั้นจะใช้ชื่ออะไร? surname1? surnameNew? หรือแย่กว่านั้น surnameV2?ผลที่ตามมาคือสกีมาที่อัปเดตแล้วจะเข้าใจยากขึ้นและละเอียดมากขึ้น |
| สกีมาอาจสะสมฟิลด์ที่ deprecated | การประกาศ deprecated ฟิลด์นั้นเหมาะสมที่สุดในฐานะสถานการณ์ชั่วคราว ในท้ายที่สุดเราต้องการลบฟิลด์เหล่านั้นออกจากสกีมาก่อนที่พวกเขาจะเริ่มสะสม แต่อาจมีไคลเอนต์ที่ไม่ได้ทบทวน queries ของตนและยังดึงข้อมูลจากฟิลด์ที่ deprecated อยู่ ในกรณีนี้ สกีมาของเราจะค่อยๆ กลายเป็นสุสานฟิลด์ที่สะสมฟิลด์ต่างๆ หลายรายการสำหรับฟังก์ชันการทำงานเดียวกัน |
ลองดูวิธีแก้ปัญหาเหล่านี้
การกำหนดเวอร์ชันของฟิลด์
เราสามารถสร้างฟิลด์ของเราด้วย argument ที่เรียกว่า version ซึ่งเราระบุว่าจะใช้เวอร์ชันไหนของฟิลด์
ในสถานการณ์นี้ เราจะยังต้องเก็บการใช้งานสำหรับฟิลด์ที่ deprecated ไว้ ดังนั้นเราไม่ได้ปรับปรุงในส่วนนั้น แต่สัญญาของมันจะถูกซ่อนไว้: ฟิลด์ใหม่สามารถคงชื่อเดิมไว้ได้ (ไม่จำเป็นต้องเปลี่ยนชื่อจาก surname เป็น personSurname) ป้องกันไม่ให้สกีมาของเราละเอียดเกินไป
โปรดทราบว่าแนวคิดการกำหนดเวอร์ชันนี้แตกต่างจากใน REST:
- REST กำหนดสถานการณ์ all-or-nothing ที่ API ที่ถูก query ทั้งหมดมีเวอร์ชันเดียวกัน เนื่องจากเวอร์ชันที่จะใช้เป็นส่วนหนึ่งของ endpoint
- ในแนวทางนี้ แต่ละฟิลด์ได้รับการกำหนดเวอร์ชันอย่างอิสระ
ดังนั้น เราสามารถเข้าถึงเวอร์ชันที่แตกต่างกันสำหรับฟิลด์ต่างๆ ได้ดังนี้:
query GetPosts {
posts(version: "1.0.0") {
id
title(version: "2.1.1")
url
author {
id
name(version: "1.5.3")
}
}
}ยิ่งไปกว่านั้น โดยการพึ่งพา semantic versioning เราสามารถใช้ข้อจำกัดเวอร์ชันเพื่อเลือกเวอร์ชัน โดยปฏิบัติตามกฎเดียวกับที่ Composer ใช้ในการประกาศ dependencies ของแพ็กเกจ จากนั้นเราเปลี่ยนชื่อ argument version เป็น versionConstraint และอัปเดต query:
query GetPosts {
posts(versionConstraint: "^1.0") {
id
title(versionConstraint: ">=2.1")
url
author {
id
name(versionConstraint: "~1.5.3")
}
}
}การนำกลยุทธ์นี้ไปใช้กับฟิลด์ surname ที่ deprecated เราสามารถติดแท็กการใช้งาน deprecated เป็นเวอร์ชัน "1.0.0" และการใช้งานใหม่เป็นเวอร์ชัน "2.0.0" และเข้าถึงทั้งสองได้แม้แต่ใน query เดียวกัน:
query GetSurname {
account(id: 1) {
oldVersion: surname(versionConstraint: "^1.0")
newVersion: surname(versionConstraint: "^2.0")
}
}ฟีเจอร์นี้มีให้ใช้งานใน Gato GraphQL:

การกำหนดเวอร์ชันของ directive
เนื่องจาก directive ก็รับ argument เช่นกัน เราสามารถนำวิธีการเดียวกันทั้งหมดมาใช้กำหนดเวอร์ชันของ directive ได้เช่นกัน!
ตัวอย่างเช่น เมื่อรัน query นี้:
query {
post(by: { id: 1 }) {
oldVersion: title @strTitleCase(versionConstraint: "^0.1")
newVersion: title @strTitleCase(versionConstraint: "^0.2")
}
}มันสามารถสร้างการตอบสนองที่แตกต่างกันสำหรับแต่ละเวอร์ชันของ directive:
