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

Argument และ Variable

จนถึงตอนนี้ query ของเราแค่ เลือก field เท่านั้น แต่งานอ่านข้อมูลจริงมักต้องการ input ด้วย เช่น track ตัวไหน, เอาผลลัพธ์ กี่รายการ, ค้นด้วยคำ อะไร GraphQL รับ input นั้นเป็นสองชั้น ชั้นแรกคือ field argument ที่ประกาศไว้บน field ชั้นที่สองคือ query variable ที่ป้อนค่าให้ argument จากนอก query string บทเรียนนี้จะเชื่อมสองชั้นนี้เข้าด้วยกัน

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 จะคืนอะไรกลับมา

การ 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"]
ประกาศ variable บน operation อ้างถึงที่ argument แล้วป้อนค่าผ่าน JSON map แยกต่างหาก

ไล่การไหลดู operation ประกาศ $id: ID! ตัว field เอา $id ไปเป็นค่าให้ argument id แล้ว variables map ป้อนค่าจริง "t1" เข้ามา ข้อความของ query คงที่ทุกครั้งที่เรียก มีแค่ variables map เท่านั้นที่เปลี่ยน

การแยกค่าออกจาก 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 เสมอ

ตัวอย่างด้านล่างประกาศ variable $id: ID! ไว้บน operation เอาไปใช้เป็น argument id ของ field track แล้วป้อนค่าผ่าน variableValues map เหมือนที่ client จริงส่งมาเป๊ะ ๆ กด Run แล้วลองเปลี่ยนค่าใน variables map ดู

JavaScript

ตัว query string พูดถึงแค่ $id ส่วนค่า "t2" อยู่ใน variableValues ฝั่ง resolver รับค่าผ่าน args แล้วไปค้น track ลองเปลี่ยน variableValues เป็น { id: 't1' } แล้วรันใหม่ ข้อความของ query อันเดิมเป๊ะ แต่ได้ track คนละตัว นี่แหละคือประเด็นทั้งหมดของ variable

ข้อดีข้อแลกเปลี่ยน
variable ป้องกัน query injectionschema ต้องประกาศ argument type อย่างชัดเจน
query reuse ได้โดยไม่ต้อง rebuild stringargument ที่ซับซ้อนต้องใช้ Input Type
client ส่ง variable แยกจาก query — cache ได้ดีกว่าdefault value ต้องประกาศใน schema ไม่ใช่ใน resolver
type validation ของ variable เกิดก่อน executionargument จำนวนมากทำให้ 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 เสมอ
argument ที่ field หนึ่งรับได้ ประกาศไว้ที่ไหน?
ใน query อย่าง query GetTrack($id: ID!) เครื่องหมาย "$" นำหน้าหมายถึงอะไร?
ทำไมถึงควรใช้ variable แทนการ hardcode ค่า literal ลงใน query string?