แนวคิด ไอเดีย และกลยุทธ์
แนวคิด ไอเดีย และกลยุทธ์การพัฒนาสกีมาด้วยการกำหนดเวอร์ชันของฟิลด์

การพัฒนาสกีมาด้วยการกำหนดเวอร์ชันของฟิลด์

เมื่อความต้องการของแอปพลิเคชันพัฒนาขึ้น 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

การพัฒนาสกีมา

ผ่านการพัฒนา ฟิลด์ที่มีการเปลี่ยนแปลงที่ทำให้เกิดความเสียหายต้องผ่านกระบวนการดังต่อไปนี้:

  1. นำฟิลด์กลับมาใช้งานใหม่โดยใช้ชื่อที่แตกต่างออกไป
  2. ประกาศให้ฟิลด์เป็น deprecated โดยขอให้ไคลเอนต์ใช้ฟิลด์ใหม่แทน
  3. เมื่อฟิลด์ไม่ถูกใช้งานโดยใครอีกต่อไป ให้ลบออกจากสกีมา

ลองดูตัวอย่าง สมมติว่าเรามีประเภท 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:

Querying fields through version constraints

การกำหนดเวอร์ชันของ directive

เนื่องจาก directive ก็รับ argument เช่นกัน เราสามารถนำวิธีการเดียวกันทั้งหมดมาใช้กำหนดเวอร์ชันของ directive ได้เช่นกัน!

ตัวอย่างเช่น เมื่อรัน query นี้:

query {
  post(by: { id: 1 }) {
    oldVersion: title @strTitleCase(versionConstraint: "^0.1")
    newVersion: title @strTitleCase(versionConstraint: "^0.2")
  }
}

มันสามารถสร้างการตอบสนองที่แตกต่างกันสำหรับแต่ละเวอร์ชันของ directive:

Querying a versioned directive