Argument และ Variable
จนถึงตอนนี้ query ของเราแค่ เลือก field เท่านั้น แต่งานอ่านข้อมูลจริงมักต้องการ input ด้วย เช่น track ตัวไหน, เอาผลลัพธ์ กี่รายการ, ค้นด้วยคำ อะไร GraphQL รับ input นั้นเป็นสองชั้น ชั้นแรกคือ field argument ที่ประกาศไว้บน field ชั้นที่สองคือ query variable ที่ป้อนค่าให้ argument จากนอก query string บทเรียนนี้จะเชื่อมสองชั้นนี้เข้าด้วยกัน
field argument
หัวข้อที่มีชื่อว่า “field argument”field ประกาศ argument ไว้ใน schema ได้ โดยเขียนในวงเล็บต่อท้ายชื่อ field แต่ละ argument มีชื่อและ type และจะเป็น non-null ก็ได้
type Query { track(id: ID!): Track search(term: String!, limit: Int): [Track!]!}field track รับ id ที่บังคับต้องมี ส่วน search รับ term ที่บังคับ และ limit ที่จะใส่หรือไม่ใส่ก็ได้ ตอน client เลือก field ก็ใส่ argument เข้าไปแบบ inline
{ track(id: "t1") { title }}ฝั่ง resolver จะได้ argument เหล่านี้มาในรูป object args ตัวเดียวกับที่เจอในบทเรียนก่อน กรณีนี้ args.id มีค่าเป็น "t1" พูดง่าย ๆ argument คือช่องทางที่ client ใช้บังคับทิศทางว่า field จะคืนอะไรกลับมา
query variable
หัวข้อที่มีชื่อว่า “query variable”การ hardcode "t1" ไว้ใน query string ก็ใช้ได้ แต่แข็งทื่อ ข้อความของ query จะเปลี่ยนทุกครั้งที่ค่าเปลี่ยน ทำให้ cache พังและเปิดช่องให้บั๊กจากการต่อ string ตัว query variable แก้ปัญหานี้ด้วยการประกาศ placeholder ที่มีชื่อและ type ไว้บน operation แล้วอ้างถึงตรงตำแหน่งที่ค่าของ argument ควรอยู่
ชื่อ variable ขึ้นต้นด้วย $ ประกาศไว้ใน signature ของ operation พร้อม type แล้วนำไปใช้เป็นค่าของ argument
query GetTrack($id: ID!) { track(id: $id) { title }}ใน query ไม่มี id แบบ literal อยู่เลย ค่าจริงเดินทางแยกมาเป็น JSON object เล็ก ๆ ที่ client ส่งไปพร้อม query
{ "id": "t1" }flowchart LR
Decl["Operation declares $id: ID!"] --> Use["Field uses track(id: $id)"]
Map["Variables map: { id: 't1' }"] -->|"supplies value"| Use
Use --> Server["Server validates type, then resolves"] ไล่การไหลดู operation ประกาศ $id: ID! ตัว field เอา $id ไปเป็นค่าให้ argument id แล้ว variables map ป้อนค่าจริง "t1" เข้ามา ข้อความของ query คงที่ทุกครั้งที่เรียก มีแค่ variables map เท่านั้นที่เปลี่ยน
ทำไม variable จึงสำคัญ
หัวข้อที่มีชื่อว่า “ทำไม variable จึงสำคัญ”การแยกค่าออกจาก query ไม่ใช่แค่เรื่องความเรียบร้อย แต่คือวิธีที่ GraphQL ระดับ production ควรทำงาน
- ใช้ซ้ำได้และ cache ได้ query string ตัวเดียวรันได้กับทุก id ทั้ง client และ server จึง cache และ pre-compile ไว้ล่วงหน้าได้
- ปลอดภัยกว่า ค่าส่งไปเป็นข้อมูลที่มี type ไม่ได้แทรกเข้าไปใน string จึงไม่มีการเขียน query ใหม่แบบ injection
- validate ได้ เพราะทุก variable มี type (
$id: ID!) server จึงปฏิเสธค่าที่ขาดหรือผิด type ตั้งแต่ก่อน resolver ตัวแรกจะได้รัน
หลักคิดคือ ใช้ literal ตอนลองเล่นใน playground ได้ แต่ค่าที่ app จริงส่งควรเป็น variable เสมอ
argument และ variable ทำงานร่วมกัน
หัวข้อที่มีชื่อว่า “argument และ variable ทำงานร่วมกัน”ตัวอย่างด้านล่างประกาศ variable $id: ID! ไว้บน operation เอาไปใช้เป็น argument id ของ field track แล้วป้อนค่าผ่าน variableValues map เหมือนที่ client จริงส่งมาเป๊ะ ๆ กด Run แล้วลองเปลี่ยนค่าใน variables map ดู
ตัว query string พูดถึงแค่ $id ส่วนค่า "t2" อยู่ใน variableValues ฝั่ง resolver รับค่าผ่าน args แล้วไปค้น track ลองเปลี่ยน variableValues เป็น { id: 't1' } แล้วรันใหม่ ข้อความของ query อันเดิมเป๊ะ แต่ได้ track คนละตัว นี่แหละคือประเด็นทั้งหมดของ variable
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| variable ป้องกัน query injection | schema ต้องประกาศ argument type อย่างชัดเจน |
| query reuse ได้โดยไม่ต้อง rebuild string | argument ที่ซับซ้อนต้องใช้ Input Type |
| client ส่ง variable แยกจาก query — cache ได้ดีกว่า | default value ต้องประกาศใน schema ไม่ใช่ใน resolver |
| type validation ของ variable เกิดก่อน execution | argument จำนวนมากทำให้ resolver signature ยาว |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”ส่ง Argument เป็น String แทน Variable อาการ:
query { search(term: "GraphQL") }แทนquery Search($t: String!) { search(term: $t) }- ทำให้ persisted query cache miss ทุกครั้ง
- ใช้ variable เสมอสำหรับ dynamic value
Argument ทุกอย่างเป็น String อาการ:
createUser(data: String!)แทนcreateUser(data: CreateUserInput!)- เสีย type safety และ validation ที่ schema ให้ฟรี
- ใช้ Input Type สำหรับ argument ที่มีหลาย field
💡 ตัวอย่างจากของจริง
GitHub API:
- ทุก mutation ใช้ Input Type —
CreateIssueInput,AddCommentInput- ทำให้ schema evolution ง่าย — เพิ่ม optional field ใน Input ได้โดยไม่ break client
Relay:
- enforce naming convention: mutation argument ต้องชื่อ
inputและเป็น Input Type เสมอ