สถาปัตยกรรม
สถาปัตยกรรมการจัดการลำดับการ resolution ของฟิลด์

การจัดการลำดับการ resolution ของฟิลด์

เป้าหมายของ directive @export ที่ Multiple Query Execution มอบให้คือการ export ค่าของฟิลด์ (หรือชุดฟิลด์) ไปยังตัวแปร เพื่อนำไปใช้ในที่อื่นในการ query

directive นี้จะไม่ทำงานหากการอ่านตัวแปรเกิดขึ้นก่อนการ export ค่าไปยังตัวแปร ดังนั้นเอนจินจึงต้องมีวิธีควบคุมลำดับการประมวลผลฟิลด์

Gato GraphQL มีวิธีจัดการลำดับการประมวลผลฟิลด์ผ่านการ query เอง เอนจินโหลดข้อมูลเป็นรอบ ๆ สำหรับแต่ละ type โดยแก้ไขฟิลด์ทั้งหมดจาก type แรกที่พบใน query ก่อน จากนั้นจึงแก้ไขฟิลด์ทั้งหมดจาก type ที่สองที่พบใน query และดำเนินการต่อเช่นนี้จนกว่าจะไม่มี type ให้ประมวลผล

ตัวอย่างเช่น query ต่อไปนี้ที่เกี่ยวข้องกับออบเจ็กต์ของ type Director, Film และ Actor:

{
  directors {
    name
    films {
      title
      actors {
        name
      }
    }
  }
}

...จะถูกแก้ไขโดย GraphQL engine ตามลำดับนี้:

การจัดการ type ในแต่ละรอบ

หากหลังจากประมวลผลแล้ว type ถูกอ้างอิงอีกครั้งใน query เพื่อดึงข้อมูลที่ยังไม่ได้โหลด (เช่น จากออบเจ็กต์เพิ่มเติม หรือฟิลด์เพิ่มเติมจากออบเจ็กต์ที่โหลดไว้แล้ว) type นั้นจะถูกเพิ่มอีกครั้งที่ส่วนท้ายของรายการรอบ

ตัวอย่างเช่น หากเรายัง query ฟิลด์ preferredDirector ของ Actor (ซึ่งส่งคืนออบเจ็กต์ของ type Director) เช่นนี้:

{
  directors {
    name
    films {
      title
      actors {
        name
        preferredDirector {
          name
        }
      }
    }
  }
}

...GraphQL engine จะประมวลผล query ตามลำดับนี้:

Type ที่ซ้ำกันในแต่ละรอบ

มาดูกันว่าสิ่งนี้จะเกิดขึ้นอย่างไรสำหรับการรัน @export ใน query เดียว สำหรับความพยายามแรก เราสร้าง query ตามปกติโดยไม่คำนึงถึงลำดับการประมวลผลของฟิลด์:

query GetPostsAuthorNames {
  user(by: { id: 1 }) {
    name @export(as: "authorName")
  }
  posts(filter: { search: $authorName }) {
    id
    title
  }
}

เมื่อรัน query จะได้รับ response นี้:

การรัน query โดยใช้ตัวแปร

...ซึ่งมีข้อผิดพลาดต่อไปนี้:

{
  "errors": [
    {
      "message": "Expression 'authorName' is undefined",
    }
  ]
}

ข้อผิดพลาดนี้หมายความว่า ในขณะที่ตัวแปร $authorName ถูกอ่าน มันยังไม่ได้ถูกกำหนดค่า นั่นคือมีค่าเป็น undefined

มาดูว่าเหตุใดสิ่งนี้จึงเกิดขึ้น ก่อนอื่น เราวิเคราะห์ว่า type ใดบ้างที่ปรากฏใน query โดยเพิ่มเป็น comment ด้านล่าง:

# Type: Root
query GetPostsAuthorNames {
  # Type: User
  user(by: {id: 1}) {
    # Type: String
    name @export(as: "authorName")
  }
  # Type: Post
  posts(filter: { search: $authorName }) {
    # Type: ID
    id
    # Type: String
    title
  }
}

เพื่อประมวลผล type และโหลดข้อมูล data-loading engine จะเพิ่ม query type Root ลงในรายการ FIFO (First-In, First-Out) ทำให้ [Root] เป็นรายการเริ่มต้นที่ส่งไปยังอัลกอริทึม จากนั้น iterate ผ่าน type ตามลำดับดังนี้:

#การดำเนินการรายการ
0เตรียมรายการ FIFO[Root]
1aนำ type แรกในรายการออก (Root)[]
1bประมวลผลฟิลด์ทั้งหมดที่ query จาก type Root:
user(by: {id: 1})
posts(filter: { search: $authorName })
เพิ่ม type ของพวกเขา (User และ Post) ลงในรายการ
[User, Post]
2aนำ type แรกในรายการออก (User)[Post]
2bประมวลผลฟิลด์ที่ query จาก type User:
name @export(as: "authorName")
เนื่องจากเป็น scalar type (String) จึงไม่จำเป็นต้องเพิ่มลงในรายการ
[Post]
3aนำ type แรกในรายการออก (Post)[]
3bประมวลผลฟิลด์ทั้งหมดที่ query จาก type Post:
id
title
เนื่องจากเป็น scalar type (ID และ String) จึงไม่จำเป็นต้องเพิ่มลงในรายการ
[]
4รายการว่างเปล่า การ iterate สิ้นสุด 

ที่นี่เราเห็นปัญหา: @export ถูกประมวลผลในขั้นตอน 2b แต่ถูกอ่านในขั้นตอน 1b

ที่นี่เองที่เราต้องควบคุมการไหลของการประมวลผลฟิลด์ วิธีแก้ที่นำมาใช้คือการหน่วงเวลาการอ่านตัวแปรที่ export ไว้ โดยทำได้ด้วยการ query ฟิลด์ self จาก type Root อย่างจงใจ

ฟิลด์ self ตามชื่อที่บ่งบอก จะส่งคืนออบเจ็กต์เดิม เมื่อนำไปใช้กับออบเจ็กต์ Root จะส่งคืนออบเจ็กต์ Root เดิม คุณอาจสงสัยว่า "ถ้าฉันมีออบเจ็กต์ root อยู่แล้ว ทำไมต้องดึงมันมาอีกครั้ง?" เพราะอัลกอริทึมของเอนจินจำเป็นต้องเพิ่มการอ้างอิงใหม่ไปยัง Root นี้ที่ส่วนท้ายของรายการ FIFO และเราสามารถกระจายฟิลด์ที่ query ไว้ก่อนหรือหลังแต่ละ iteration เหล่านี้ได้อย่างจงใจ

นั่นคือเหตุผลที่ฟิลด์ posts(filter:{ search: $authorName }) ถูกวางไว้ภายในฟิลด์ self ใน query ด้านบน และการรัน query จะได้รับ response ที่คาดหวัง:

query GetPostsAuthorNames {
  user(by: {id: 1}) {
    name @export(as: "authorName")
  }
  self {
    posts(filter: { search: $authorName }) {
      id
      title
    }
  }
}

การรัน query แรกพร้อม @export

มาสำรวจลำดับการประมวลผล type ใน query นี้เพื่อทำความเข้าใจว่าทำไมมันจึงทำงานได้ดี:

#การดำเนินการรายการ
0เตรียมรายการ FIFO[Root]
1aนำ type แรกในรายการออก (Root)[]
1bประมวลผลฟิลด์ทั้งหมดที่ query จาก type Root:
user(by: {id: 1})
self
เพิ่ม type ของพวกเขา (User และ Root) ลงในรายการ
[User, Root]
2aนำ type แรกในรายการออก (User)[Root]
2bประมวลผลฟิลด์ที่ query จาก type User:
name @export(as: "authorName")
เนื่องจากเป็น scalar type (String) จึงไม่จำเป็นต้องเพิ่มลงในรายการ
[Root]
3aนำ type แรกในรายการออก (Root)[]
3bประมวลผลฟิลด์ที่ query จาก type Root:
posts(filter:{ search: $authorName })
เพิ่ม type ของมัน (Post) ลงในรายการ
[Post]
4aนำ type แรกในรายการออก (Post)[]
4bประมวลผลฟิลด์ทั้งหมดที่ query จาก type Post:
id
title
เนื่องจากเป็น scalar type (ID และ String) จึงไม่จำเป็นต้องเพิ่มลงในรายการ
[]
5รายการว่างเปล่า การ iterate สิ้นสุด 

ตอนนี้เราเห็นแล้วว่าปัญหาได้รับการแก้ไขแล้ว: @export ถูกประมวลผลในขั้นตอน 2b และถูกอ่านในขั้นตอน 3b

Multiple Query Execution ทำสิ่งนี้ทุกครั้งเมื่อ แยก queries: มันแปลง GraphQL document โดยเพิ่มฟิลด์ self เพื่อให้ฟิลด์ในแต่ละ operation ถูกประมวลผลเฉพาะหลังจากที่ฟิลด์ทั้งหมดในทุก operation ก่อนหน้าได้รับการแก้ไขแล้ว