ข้ามไปยังเนื้อหา

Query และ Resolver

ในโมดูล foundations คุณได้เรียนรู้ว่า schema คือ contract และ query คือเส้นทางที่ลากผ่าน graph ของ type ต่าง ๆ โมดูลนี้พูดถึงสองส่วนที่ทำให้เส้นทางนั้นมีชีวิต คือ query ที่ร้องขอข้อมูล และ resolver ที่สร้างข้อมูลขึ้นมา ตัว query เป็นเจตนาล้วน ๆ ระบุแค่ชื่อ field แต่ไม่บอกว่าค่ามาจากไหน ส่วน resolver คือ logic ฝั่ง server ที่รันทีละ field เพื่อเติมค่าจริงให้ชื่อเหล่านั้น

แยกสองบทบาทนี้ออกจากกันให้ชัดในใจ แล้วทุกอย่างที่เหลือจะตามมาเอง

  • query คือ operation สำหรับอ่านที่ฝั่ง client เขียนขึ้น เลือก field และส่ง argument ได้ แต่ไม่มี logic อยู่ข้างใน เทียบได้กับรายการของที่จะซื้อ
  • resolver คือ function ฝั่ง server ที่ผูกกับ field เพียงตัวเดียว เมื่อการ execute มาถึง field นั้น resolver จะรันและคืนค่ากลับมา เทียบได้กับพนักงานคลังสินค้าที่ไปหยิบของทีละชิ้นตามรายการ

server ไม่เคยเดา แต่จะเรียก resolver ที่ตรงกับทุก field ใน query รวบรวมค่าที่ได้ แล้วประกอบเป็น JSON response ที่มีรูปร่างเหมือน query เป๊ะ

ทุกการอ่านต้องเริ่มจากที่ใดที่หนึ่ง จุดเริ่มนั้นคือ root object type พิเศษชื่อ Query บรรดา field ที่อยู่ใต้ Query โดยตรงคือ จุดเข้า (entry point) ของ API และเป็นที่เดียวที่การอ่านเริ่มได้ ถัดจากนั้น field เหล่านี้คืน object type ที่มี field ของตัวเอง พาคุณลึกเข้าไปใน graph ต่อได้

type Query {
currentUser: User
track(id: ID!): Track
}
type User {
id: ID!
name: String!
}
type Track {
id: ID!
title: String!
}

client อาจเริ่มการอ่านด้วย currentUser หรือ track เพราะทั้งสองเป็น field ของ Query แต่จะเริ่มด้วย title ไม่ได้ เพราะ title อยู่บน Track ที่ลึกเข้าไปใน graph คุณจะเข้าถึง title ได้ก็ต่อเมื่อก้าวผ่าน field ที่คืน Track มาก่อน

การ resolve ไม่ใช่ function ก้อนใหญ่ก้อนเดียว แต่เป็น chain ของ function เล็ก ๆ การ execute เริ่มที่ field ระดับบนสุดของ query (ที่ Query root) รัน resolver ของ field นั้น แล้วลงลึกไปยัง sub-field ที่เลือกไว้ทีละตัว รัน resolver ของแต่ละตัวไปเรื่อย ๆ จนทุก selection จบที่ scalar ค่าที่ resolver ตัวแม่ (parent) คืนมาจะกลายเป็น input ของ resolver ตัวลูก และนั่นคือเส้นด้ายที่ร้อย chain เข้าด้วยกัน

flowchart TD
  Q["Query root"] -->|"track(id) resolver"| T["Track object"]
  T -->|"title resolver"| Title["title: String"]
  T -->|"artist resolver"| A["Artist object"]
  A -->|"name resolver"| Name["name: String"]
การ execute เดินจาก Query root ลงไปผ่าน resolver ของแต่ละ field

อ่าน diagram จากบนลงล่าง เริ่มจาก resolver ของ field track ที่รันก่อนและคืน track object จากนั้น engine จะ resolve sub-field ที่ query ขอมาทีละตัว คือ title ที่ได้ค่าตรง ๆ และ artist ที่ resolver คืน artist object ออกมา แล้ว engine ก็ resolve ต่ออีกหนึ่งระดับเป็น name แต่ละกล่องในภาพคือการเรียก resolver หนึ่งครั้ง ส่วนแต่ละลูกศรคือค่าจาก parent ที่ไหลลงไปหาลูก

เมื่อจบทั้งห้าบทเรียนนี้ คุณจะสามารถเขียน query ด้วยมือเองได้ และเขียน resolver ที่ตอบ query เหล่านั้นได้ บทเรียนมีดังนี้

  1. Query และ Resolver (คุณอยู่ตรงนี้) — การแยก อ่าน/สร้าง, Query root, และ resolver chain
  2. การเขียน Query — field, nested selection set, และ operation name
  3. Resolver Function — argument สี่ตัวที่ทุก resolver ได้รับ และ default resolver ทำงานอย่างไร
  4. Argument และ Variable — field argument และ query variable ที่นำกลับมาใช้ซ้ำได้ เช่น $id
  5. Fragment และ Alias — การนำ selection set กลับมาใช้ซ้ำ และการเปลี่ยนชื่อ field ใน response

ตัวอย่างด้านล่างนิยาม Query ที่มีจุดเข้าเดียวคือ track และ resolver ของ field นี้คืน track object ออกมา จากนั้น engine จะ resolve field artist ของ track ต่อ แล้วจึง resolve name ของ artist กด Run เพื่อดู query ตัวเดียววิ่งผ่าน chain ทั้งเส้น

JavaScript

output สะท้อน query ออกมาตรง ๆ โดย track เก็บ title กับ artist ที่ซ้อนอยู่ ส่วน artist เก็บ name ไม่มีอะไรใน response ที่โผล่มาโดยบังเอิญ ทุกค่ามาจาก resolver ที่ engine เรียกระหว่างเดินตาม chain จาก Query root ลงไปจนถึง leaf

query กับ resolver ต่างกันอย่างไร?
ทุก operation สำหรับอ่านต้องเริ่มที่ไหน?
ใน resolver chain นั้น resolver ของ field ตัวแม่ส่งอะไรต่อให้ resolver ของ field ตัวลูก?