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

Relationships

ในฐานข้อมูลเชิงสัมพันธ์ คุณแสดงการเชื่อมโยงด้วย foreign key และ join table ส่วนใน GraphQL การเชื่อมโยง เดียวกัน ตรงไปตรงมากว่ามาก คือ relationship เป็นเพียง field ที่มี type เป็น type อีกตัว ไม่มี syntax พิเศษ ระบบ type ที่คุณรู้อยู่แล้วเพียงพอ

สิ่งที่เปลี่ยนไปจาก relationship หนึ่งสู่อีกอันมีเพียง รูปร่าง ของ field คือค่าเดี่ยว, list หรือ list บนทั้งสองฝั่ง

การเชื่อมโยงที่ง่ายที่สุดคืนค่าเป็น 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 เดินได้ทั้งสองทิศทาง

เมื่อทั้งสองฝั่งมีอีกฝั่งได้หลายอัน คุณจะได้ 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
one-to-many ระหว่าง Author กับ Post; many-to-many ระหว่าง Post กับ Tag

การประกาศ field เป็นเพียงครึ่งหนึ่งของงาน อีกครึ่งคือ resolver ที่ดึงข้อมูลอีกฝั่งมาให้ตอน client เลือก field นั้นจริง ๆ

resolver ของ field ที่เป็น relationship จะได้รับ object parent มา แล้วใช้ id ของ parent ค้นหา record ที่เชื่อมต่ออยู่ และเพราะ GraphQL รัน resolver ต่อเมื่อมีคนขอ field นั้น ดังนั้นถ้า client ขอ name แต่ไม่ขอ posts ก็ไม่เสียต้นทุนโหลด post เลย

schema ด้านล่างเชื่อม one-to-many (Authorposts) เข้ากับ many-to-many (PostTag) พร้อม resolver ที่ค้นหาอีกฝั่งด้วย id ตัว query ข้ามสาม relationship ในหนึ่ง request กดปุ่ม Run

JavaScript

ไล่ดูผลลัพธ์: จาก author หนึ่งคน เรา resolve posts แบบ one-to-many และภายในแต่ละ post เรา resolve tags แบบ many-to-many

ทุก field ที่เป็น relationship มี resolver ของตัวเองที่ค้นหา record ปลายทางด้วย id สังเกตว่าเราไม่เคยเขียน SQL JOIN เลย — กราฟ คือ การ join ที่แสดงออกผ่านการเลือก field แบบซ้อนกัน

ข้อดีข้อแลกเปลี่ยน
query relationship ได้ในครั้งเดียว — ไม่ต้อง join ที่ clientdeep relationship นำไปสู่ N+1 ถ้าไม่ใช้ DataLoader
schema แสดง relationship ชัดเจน — เข้าใจ data model ได้จาก schemacircular relationship อาจ query วนซ้ำไม่รู้จบ
lazy load — query เฉพาะ relationship ที่ต้องการbidirectional relationship ต้องระวัง infinite loop ใน resolver
resolver แต่ละ field ทำงาน independentrelationship ที่ไม่มี 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
relationship แบบ one-to-many อย่าง author ที่มี post หลายอัน เขียนใน SDL อย่างไร?
relationship แบบ many-to-many ระหว่าง Post กับ Tag ปรากฏใน schema อย่างไร?
อะไรทำหน้าที่ดึงข้อมูลอีกฝั่งของ field ที่เป็น relationship?