Relationships
ในฐานข้อมูลเชิงสัมพันธ์ คุณแสดงการเชื่อมโยงด้วย foreign key และ join table ส่วนใน GraphQL การเชื่อมโยง เดียวกัน ตรงไปตรงมากว่ามาก คือ relationship เป็นเพียง field ที่มี type เป็น type อีกตัว ไม่มี syntax พิเศษ ระบบ type ที่คุณรู้อยู่แล้วเพียงพอ
สิ่งที่เปลี่ยนไปจาก relationship หนึ่งสู่อีกอันมีเพียง รูปร่าง ของ field คือค่าเดี่ยว, list หรือ list บนทั้งสองฝั่ง
One-to-one และ one-to-many
หัวข้อที่มีชื่อว่า “One-to-one และ one-to-many”การเชื่อมโยงที่ง่ายที่สุดคืนค่าเป็น object เดี่ยว Post หนึ่งอันเป็นของ Author คนเดียวเท่านั้น type ของ field จึงเป็นแค่ Author:
type Post { id: ID! title: String! author: Author!}ทิศทางย้อนกลับเป็นแบบ one-to-many: author หนึ่งคนมี post หลายอัน type ของ field จึงกลายเป็น list คือ [Post!]!:
type Author { id: ID! name: String! posts: [Post!]!}นั่นคือรูปแบบทั้งหมด ฝั่ง “to-one” คืนค่าเป็น type ส่วนฝั่ง “to-many” คืนค่าเป็น list ของ type นั้น และ type คู่เดียวกันเปิดเผย relationship ได้จากทั้งสองปลาย ทำให้ client เดินได้ทั้งสองทิศทาง
Many-to-many
หัวข้อที่มีชื่อว่า “Many-to-many”เมื่อทั้งสองฝั่งมีอีกฝั่งได้หลายอัน คุณจะได้ relationship แบบ many-to-many เช่น Post หนึ่งอันถือ Tag หลายอัน และแต่ละ Tag ก็ติดอยู่กับ post หลายอัน ใน SDL คือ field ที่เป็น list บน type แต่ละตัว:
type Post { id: ID! title: String! tags: [Tag!]!}
type Tag { id: ID! label: String! posts: [Post!]!}ไม่มี “join table” โผล่ใน schema — งานเบื้องหลังอยู่ใน data layer ของคุณ ส่วน client เห็นเพียง type สองตัว แต่ละตัวมี field ที่เป็น list ชี้ไปยังอีกตัว
flowchart LR Author["type Author"] Post["type Post"] Tag["type Tag"] Author -->|"posts: [Post!]! (one-to-many)"| Post Post -->|"author: Author!"| Author Post -->|"tags: [Tag!]! (many-to-many)"| Tag Tag -->|"posts: [Post!]!"| Post
การ resolve อีกฝั่งหนึ่ง
หัวข้อที่มีชื่อว่า “การ resolve อีกฝั่งหนึ่ง”การประกาศ field เป็นเพียงครึ่งหนึ่งของงาน อีกครึ่งคือ resolver ที่ดึงข้อมูลอีกฝั่งมาให้ตอน client เลือก field นั้นจริง ๆ
resolver ของ field ที่เป็น relationship จะได้รับ object parent มา แล้วใช้ id ของ parent ค้นหา record ที่เชื่อมต่ออยู่ และเพราะ GraphQL รัน resolver ต่อเมื่อมีคนขอ field นั้น ดังนั้นถ้า client ขอ name แต่ไม่ขอ posts ก็ไม่เสียต้นทุนโหลด post เลย
การข้าม relationship รันจริง
หัวข้อที่มีชื่อว่า “การข้าม relationship รันจริง”schema ด้านล่างเชื่อม one-to-many (Author → posts) เข้ากับ many-to-many (Post ↔ Tag) พร้อม resolver ที่ค้นหาอีกฝั่งด้วย id ตัว query ข้ามสาม relationship ในหนึ่ง request กดปุ่ม Run
ไล่ดูผลลัพธ์: จาก author หนึ่งคน เรา resolve posts แบบ one-to-many และภายในแต่ละ post เรา resolve tags แบบ many-to-many
ทุก field ที่เป็น relationship มี resolver ของตัวเองที่ค้นหา record ปลายทางด้วย id สังเกตว่าเราไม่เคยเขียน SQL JOIN เลย — กราฟ คือ การ join ที่แสดงออกผ่านการเลือก field แบบซ้อนกัน
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| query relationship ได้ในครั้งเดียว — ไม่ต้อง join ที่ client | deep relationship นำไปสู่ N+1 ถ้าไม่ใช้ DataLoader |
| schema แสดง relationship ชัดเจน — เข้าใจ data model ได้จาก schema | circular relationship อาจ query วนซ้ำไม่รู้จบ |
| lazy load — query เฉพาะ relationship ที่ต้องการ | bidirectional relationship ต้องระวัง infinite loop ใน resolver |
| resolver แต่ละ field ทำงาน independent | relationship ที่ไม่มี DataLoader อาจทำให้ query ช้ามาก |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Circular Relationship ที่ไม่มี Depth Limit อาการ:
User.posts.author.posts.author.posts...— query วนซ้ำได้ไม่จำกัด- malicious client ส่ง deeply nested query ทำให้ server overload
- ต้องมี query depth limit (Rule: depth-and-complexity)
Relationship ที่ Return ทั้ง Collection โดยไม่มี Pagination อาการ:
user { posts { title } }— return โพสท์ทั้งหมดของ user- user ที่มีโพสท์หมื่นชิ้น ทำให้ query ช้ามาก
- relationship collection ควรมี pagination argument เสมอ:
posts(first: 10)
💡 ตัวอย่างจากของจริง
GitHub:
- ทุก relationship collection paginated:
user { repositories(first: 20) { ... } }- ไม่มี relationship ที่ return unbounded list
Prisma:
- ORM ที่ใช้คู่กับ GraphQL — relationship query ทำผ่าน
includeและselect- ใช้ร่วมกับ DataLoader เพื่อป้องกัน N+1