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

Resolver Function

resolver ก็แค่ function ธรรมดา เพียงแต่มี signature ตายตัวที่รู้กันดี ขอแค่จำ argument สี่ตัวกับลำดับได้ คุณก็เขียน resolver ให้ field ไหนบน type ไหนก็ได้ บทนี้จะแกะ signature นั้นออกมาดู แล้วไล่ต่อว่า resolver ต่อกันเป็น chain อย่างไร

ทุก resolver ถูกเรียกด้วย argument สี่ตัวเดิม เรียงลำดับนี้เสมอ

function resolve(parent, args, context, info) {
// ...return the field's value
}
  • parent — ค่าที่ resolver ชั้นเหนือขึ้นไปหนึ่งระดับคืนมา ถ้าเป็น field บน Query ระดับบนสุดจะไม่มี parent object ค่าที่ได้จึงเป็น root value (ส่วนใหญ่ไม่ได้ใช้) ส่วน User.name จะได้ User object ที่ field ชั้นบน resolve มาแล้ว
  • args — object ที่เก็บ argument ที่ query ส่งให้ field ตัวนี้ ถ้า field คือ track(id: ID!) ค่า args.id ก็คือสิ่งที่ client ส่งมา
  • context — ค่าที่ resolver ทุกตัวใน request เดียวกันใช้ร่วมกัน server สร้างขึ้นครั้งเดียวต่อ request แล้วส่ง object ตัวเดิมให้ resolver ทุกตัว ตรงนี้คือที่เก็บของระดับ request เช่น user ที่ผ่าน authenticate, connection ของ database, logger
  • info — metadata ของการ execute เช่น ชื่อ field, path จาก root, query AST ทั้งก้อน งานประจำวันแทบไม่ได้แตะ แต่มีไว้สำหรับเคสขั้นสูง

สองตัวแรกคือ parent กับ args เป็นตัวที่ใช้หนักที่สุด ตัว parent บอกว่า กำลัง resolve field ของอะไรอยู่ ส่วน args บอกว่า client ขออะไรมา

resolver ไม่ได้ทำงานตัวใครตัวมัน แต่รันต่อกันเป็น chain ตามรูปร่างของ query tree engine จะ resolve field ตัวพ่อก่อน เอาค่าที่ได้ไปส่งเป็น argument parent ให้ resolver ของ field ลูกทุกตัว การส่งต่อแค่ขั้นเดียวนี้แหละที่เย็บ graph ทั้งอันเข้าด้วยกัน

flowchart TD
  Root["Query root value"] -->|"track resolver returns track"| T["track object = parent"]
  T -->|"artist resolver returns artist"| A["artist object = parent"]
  A -->|"name resolver reads parent.name"| N["name: String"]
ค่าที่ resolver แต่ละตัวคืนมา กลายเป็น argument parent ของ field ลูก

ไล่ตามลูกศรดู resolver ของ track คืน track object ออกมา จากนั้น engine เรียก resolver ของ artist โดยส่ง track ตัวนั้นไปเป็น parent ตัว resolver ของ artist คืน artist object ต่อ สุดท้าย resolver ของ name ถูกเรียกโดยได้ artist เป็น parent แล้วอ่าน parent.name สรุปคือค่าที่คืนจากข้อต่อหนึ่ง กลายเป็น parent ของข้อต่อถัดไป

คุณไม่ต้องเขียน resolver ให้ครบทุก field เมื่อ field ไหนไม่มี resolver ระบุไว้ GraphQL จะถอยไปใช้ default resolver ซึ่งทำงานตรงไปตรงมามาก คือมองไปที่ parent object แล้วคืน property ที่ชื่อตรงกับ field นั้น ดังนั้นถ้า resolver ของ track คืน { id, title, artist } engine จะ resolve title ให้เองด้วยการอ่าน parent.title โดยไม่ต้องมี resolver ของ title เลย

นี่คือเหตุผลที่ schema เล็ก ๆ มักเขียน resolver แค่บน Query root ก็พอ root resolver คืน object ที่เติมข้อมูลครบ แล้ว default resolver ก็ไล่เก็บส่วนที่เหลือเอง คุณค่อยเขียน resolver แบบระบุชัดเฉพาะ field ที่ต้องคำนวณ ต้องดึงข้อมูลเพิ่ม หรือต้องปรับรูปร่าง

ตัวอย่างด้านล่างใช้ building block เต็ม ๆ ของ graphql เพื่อเขียน resolver function ออกมาให้เห็นชัด ทุกตัวโชว์ signature (parent, args) ของตัวเอง resolver ของ Query.track สร้าง track ขึ้นมา resolver ของ Track.artist รับ track นั้นมาเป็น parent แล้ว resolver ของ Artist.name ก็อ่านจาก parent ของตัวเองต่อ กด Run ได้เลย

JavaScript

สังเกตว่า id กับ title ไม่มี function resolve ระบุไว้เลย default resolver อ่านค่าตรง ๆ จาก parent object ที่ resolver ของ track คืนมา ส่วน resolver ของ artist แค่ส่งต่อ parent.artist ซึ่งกลายไปเป็น parent ของ name ต่อ สรุปคือ resolver แต่ละตัวมองขึ้นไปแค่ระดับเดียว และสนใจแค่ field ของตัวเอง

ข้อดีข้อแลกเปลี่ยน
แต่ละ field มี resolver อิสระ — เปลี่ยน data source ได้โดยไม่กระทบ schemaresolver จำนวนมากใน schema ใหญ่ทำให้ manage ยาก
context injection ทำให้แชร์ auth, db, logger ข้าม resolverN+1 problem เกิดได้ง่ายถ้าไม่ใช้ DataLoader
resolver chain ทำให้ต่อ data source หลายชนิดได้resolver ที่ logic หนักทำให้ debug ยาก
default resolver ลด boilerplate สำหรับ simple fieldtest resolver ต้องการ mock context ที่ซับซ้อน

Business Logic หนักใน Resolver อาการ:

  • resolver มีทั้ง validation, authorization, database query, transformation ใน function เดียว
  • test ยากเพราะ side effect มาก
  • แยก: resolver ควรเรียก service/repository แค่ 1-2 บรรทัด

ไม่ใช้ DataLoader สำหรับ Relationship อาการ:

  • resolver ของ Post.author ทำ db.findUser(post.authorId) ทุก post
  • 100 post = 100 database query (N+1)
  • ใช้ DataLoader batch query ทั้งหมดเป็น 1 request

💡 ตัวอย่างจากของจริง

GraphQL Yoga:

  • ใช้ resolver chain ต่อกับ database, REST API, cache ได้ใน resolver เดียว

Prisma:

  • ORM ยอดนิยมสำหรับ GraphQL resolver
  • prisma.user.findUnique() ใน resolver ทำให้ code สั้นและ type-safe
resolver ได้รับ argument สี่ตัวมาตามลำดับใด?
argument "parent" ของ field resolver มาจากไหน?
เมื่อ field ไม่มี resolve function ระบุไว้ default resolver ทำอะไร?