GraphQL vs. REST - บทช่วยสอน GraphQL
เผยแพร่แล้ว: 2022-03-11คุณอาจเคยได้ยินเกี่ยวกับเด็กใหม่ในกลุ่มนี้: GraphQL หากไม่เป็นเช่นนั้น GraphQL เป็นวิธีใหม่ในการดึงข้อมูล API ซึ่งเป็นทางเลือกใหม่สำหรับ REST มันเริ่มต้นเป็นโครงการภายในที่ Facebook และเนื่องจากเป็นโอเพ่นซอร์สจึงได้รับแรงฉุดอย่างมาก
จุดมุ่งหมายของบทความนี้คือการช่วยให้คุณเปลี่ยนจาก REST เป็น GraphQL ได้อย่างง่ายดาย ไม่ว่าคุณจะตัดสินใจเกี่ยวกับ GraphQL แล้วหรือคุณเพียงแค่เต็มใจที่จะลองดู ไม่จำเป็นต้องมีความรู้เกี่ยวกับ GraphQL มาก่อน แต่จำเป็นต้องมีความคุ้นเคยกับ REST API เพื่อทำความเข้าใจบทความ
ส่วนแรกของบทความจะเริ่มต้นด้วยการให้เหตุผลสามประการว่าทำไมฉันถึงคิดว่า GraphQL ดีกว่า REST โดยส่วนตัว ส่วนที่สองเป็นบทช่วยสอนเกี่ยวกับวิธีเพิ่มตำแหน่งข้อมูล GraphQL ในส่วนแบ็คเอนด์ของคุณ
Graphql กับ REST: ทำไมต้องวาง REST
หากคุณยังคงลังเลว่า GraphQL นั้นเหมาะสมกับความต้องการของคุณหรือไม่ ภาพรวมที่ค่อนข้างกว้างขวางและเป็นกลางของ “REST vs. GraphQL” จะแสดงไว้ที่นี่ อย่างไรก็ตาม ด้วยเหตุผลสามอันดับแรกของฉันในการใช้ GraphQL โปรดอ่านต่อ
เหตุผลที่ 1: ประสิทธิภาพของเครือข่าย
สมมติว่าคุณมีทรัพยากรผู้ใช้ที่ส่วนหลังซึ่งมีชื่อ นามสกุล อีเมล และช่องอื่นๆ อีก 10 ช่อง สำหรับไคลเอนต์ โดยทั่วไปคุณต้องการเพียงสองสามอย่างเท่านั้น
การโทร REST ที่ปลายทาง /users
จะทำให้คุณมีฟิลด์ทั้งหมดของผู้ใช้กลับมา และไคลเอ็นต์จะใช้เฉพาะฟิลด์ที่จำเป็นเท่านั้น เห็นได้ชัดว่ามีการสูญเสียข้อมูลในการถ่ายโอนซึ่งอาจเป็นข้อควรพิจารณาเกี่ยวกับไคลเอนต์มือถือ
โดยค่าเริ่มต้น GraphQL จะดึงข้อมูลที่เล็กที่สุดเท่าที่จะเป็นไปได้ ถ้าคุณต้องการเพียงชื่อและนามสกุลของผู้ใช้ของคุณ คุณต้องระบุในแบบสอบถามของคุณ
อินเทอร์เฟซด้านล่างเรียกว่า GraphiQL ซึ่งเหมือนกับตัวสำรวจ API สำหรับ GraphQL ฉันสร้างโครงการขนาดเล็กเพื่อจุดประสงค์ของบทความนี้ รหัสถูกโฮสต์บน GitHub และเราจะเจาะลึกในส่วนที่สอง
ที่บานหน้าต่างด้านซ้ายของอินเทอร์เฟซคือแบบสอบถาม ที่นี่ เรากำลังดึงข้อมูลผู้ใช้ทั้งหมด—เราจะทำ GET /users
ด้วย REST—และรับเฉพาะชื่อและนามสกุลเท่านั้น
แบบสอบถาม
query { users { firstname lastname } }
ผลลัพธ์
{ "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }
หากเราต้องการรับอีเมลด้วย การเพิ่มบรรทัด "อีเมล" ใต้ "นามสกุล" จะช่วยได้
แบ็กเอนด์ REST บางตัวมีตัวเลือกต่างๆ เช่น /users?fields=firstname,lastname
เพื่อส่งคืนทรัพยากรบางส่วน สำหรับสิ่งที่คุ้มค่า Google ขอแนะนำ อย่างไรก็ตาม ไม่ได้ใช้งานโดยค่าเริ่มต้น และทำให้คำขออ่านแทบไม่ได้ โดยเฉพาะอย่างยิ่งเมื่อคุณใส่พารามิเตอร์การสืบค้นอื่นๆ:
-
&status=active
เพื่อกรองผู้ใช้ที่ใช้งานอยู่ -
&sort=createdAat
เพื่อจัดเรียงผู้ใช้ตามวันที่สร้าง -
&sortDirection=desc
เพราะคุณต้องการมัน -
&include=projects
ที่จะรวมโครงการของผู้ใช้
พารามิเตอร์การค้นหาเหล่านี้เป็นแพตช์ที่เพิ่มใน REST API เพื่อเลียนแบบภาษาการสืบค้น GraphQL เหนือสิ่งอื่นใดคือภาษาที่ใช้ค้นหา ซึ่งทำให้คำขอกระชับและแม่นยำตั้งแต่ต้น
เหตุผลที่ 2: ตัวเลือกการออกแบบ "รวมเทียบกับปลายทาง"
ลองนึกภาพว่าเราต้องการสร้างเครื่องมือการจัดการโครงการอย่างง่าย เรามีทรัพยากรสามอย่าง: ผู้ใช้ โครงการ และงาน เรายังกำหนดความสัมพันธ์ต่อไปนี้ระหว่างทรัพยากร:
นี่คือจุดสิ้นสุดบางส่วนที่เราเปิดเผยต่อโลก:
ปลายทาง | คำอธิบาย |
---|---|
GET /users | รายชื่อผู้ใช้ทั้งหมด |
GET /users/:id | รับผู้ใช้คนเดียวด้วย id :id |
GET /users/:id/projects | รับโปรเจ็กต์ทั้งหมดของผู้ใช้คนเดียว |
ปลายทางนั้นเรียบง่าย อ่านง่าย และมีการจัดระเบียบอย่างดี
สิ่งต่างๆ จะซับซ้อนขึ้นเมื่อคำขอของเรามีความซับซ้อนมากขึ้น ลองใช้ปลายทาง GET /users/:id/projects
: สมมติว่าฉันต้องการแสดงเฉพาะชื่อโครงการในโฮมเพจ แต่โปรเจ็กต์+งานบนแดชบอร์ด โดยไม่ต้องทำการเรียก REST หลายครั้ง ฉันจะโทร:
-
GET /users/:id/projects
สำหรับโฮมเพจ -
GET /users/:id/projects?include=tasks
(ตัวอย่าง) ในหน้าแดชบอร์ดเพื่อให้ส่วนหลังผนวกงานที่เกี่ยวข้องทั้งหมด
เป็นเรื่องปกติในการเพิ่มพารามิเตอร์การสืบค้น ?include=...
เพื่อให้ใช้งานได้ และยังได้รับการแนะนำโดยข้อกำหนด JSON API พารามิเตอร์การค้นหา เช่น ?include=tasks
ยังคงสามารถอ่านได้ แต่อีกไม่นาน เราจะจบลงด้วย ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author
ในกรณีนี้ จะดีกว่าไหมที่จะสร้างจุดปลาย /projects
เพื่อทำเช่นนี้? บางอย่างเช่น /projects?userId=:id&include=tasks
เนื่องจากเราจะมีความสัมพันธ์น้อยกว่าที่จะรวมไว้หนึ่งระดับ หรือที่จริงแล้วปลายทาง /tasks?userId=:id
อาจใช้งานได้เช่นกัน นี่อาจเป็นตัวเลือกการออกแบบที่ยาก และอาจซับซ้อนกว่านั้นหากเรามีความสัมพันธ์แบบกลุ่มต่อกลุ่ม
GraphQL ใช้วิธีการ include
ทุกที่ ทำให้ไวยากรณ์ในการดึงความสัมพันธ์มีประสิทธิภาพและสม่ำเสมอ
นี่คือตัวอย่างการดึงโปรเจ็กต์และงานทั้งหมดจากผู้ใช้ที่มี id 1
แบบสอบถาม
{ user(id: 1) { projects { name tasks { description } } } }
ผลลัพธ์
{ "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }
อย่างที่คุณเห็น ไวยากรณ์คิวรีสามารถอ่านได้ง่าย หากเราต้องการเจาะลึกลงไปและรวมงาน ความคิดเห็น รูปภาพ และผู้แต่ง เราจะไม่คิดซ้ำเลยว่าจะจัดระเบียบ API ของเราอย่างไร GraphQL ทำให้ง่ายต่อการดึงวัตถุที่ซับซ้อน
เหตุผลที่ 3: การจัดการลูกค้าประเภทต่างๆ
เมื่อสร้างแบ็คเอนด์ เรามักจะเริ่มต้นด้วยการพยายามทำให้ API ใช้งานได้อย่างกว้างขวางโดยลูกค้าทุกรายมากที่สุด แต่ลูกค้ามักต้องการโทรน้อยลงและเรียกเงินมากขึ้น ด้วยการรวมเชิงลึก ทรัพยากรบางส่วน และการกรอง คำขอจากเว็บและไคลเอนต์มือถืออาจแตกต่างกันมาก
ด้วย REST มีวิธีแก้ปัญหาสองสามข้อ เราสามารถสร้างจุดปลายแบบกำหนดเองได้ (เช่น จุดปลายนามแฝง เช่น /mobile_user
) การแสดงข้อมูลแบบกำหนดเอง ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json
) หรือแม้แต่ไคลเอนต์ -API เฉพาะ (เช่น Netflix เคยทำ) ทั้งสามต้องใช้ความพยายามเป็นพิเศษจากทีมพัฒนาส่วนหลัง
GraphQL มอบพลังให้กับลูกค้ามากขึ้น หากลูกค้าต้องการคำขอที่ซับซ้อน ลูกค้าจะสร้างการสืบค้นที่เกี่ยวข้องเอง ลูกค้าแต่ละรายสามารถใช้ API เดียวกันได้แตกต่างกัน
วิธีเริ่มต้นกับ GraphQL
ในการโต้วาทีส่วนใหญ่เกี่ยวกับ "GraphQL vs. REST" ในปัจจุบัน ผู้คนคิดว่าพวกเขาต้องเลือกอย่างใดอย่างหนึ่งจากสองอย่างนี้ นี้เป็นเพียงไม่เป็นความจริง.
แอปพลิเคชันสมัยใหม่มักใช้บริการต่างๆ ที่หลากหลาย ซึ่งเปิดเผย API ต่างๆ จริงๆ แล้ว เราสามารถคิดว่า GraphQL เป็นเกตเวย์หรือตัวห่อหุ้มสำหรับบริการเหล่านี้ทั้งหมด ลูกค้าทั้งหมดจะไปถึงปลายทางของ GraphQL และปลายทางนี้จะไปถึงชั้นฐานข้อมูล บริการภายนอก เช่น ElasticSearch หรือ Sendgrid หรือตำแหน่งข้อมูล REST อื่นๆ
วิธีที่สองในการใช้ทั้งสองอย่างคือการมีจุดปลาย /graphql
แยกต่างหากใน REST API ของคุณ สิ่งนี้มีประโยชน์อย่างยิ่งหากคุณมีลูกค้าจำนวนมากที่กด REST API ของคุณอยู่แล้ว แต่คุณต้องการลองใช้ GraphQL โดยไม่กระทบต่อโครงสร้างพื้นฐานที่มีอยู่ และนี่คือโซลูชันที่เรากำลังสำรวจอยู่ในปัจจุบัน
ดังที่ได้กล่าวไว้ก่อนหน้านี้ ฉันจะอธิบายบทช่วยสอนนี้ด้วยโปรเจ็กต์ตัวอย่างเล็กๆ ที่มีอยู่ใน GitHub เป็นเครื่องมือการจัดการโครงการอย่างง่ายที่มีผู้ใช้ โครงการ และงานต่างๆ
เทคโนโลยีที่ใช้สำหรับโครงการนี้คือ Node.js และ Express สำหรับเว็บเซิร์ฟเวอร์, SQLite เป็นฐานข้อมูลเชิงสัมพันธ์ และ Sequelize เป็น ORM โมเดลสามแบบ ได้แก่ ผู้ใช้ โปรเจ็กต์ และงาน ถูกกำหนดไว้ในโฟลเดอร์ models
จุดปลาย REST /api/users
, /api/projects
และ /api/tasks
ถูกเปิดเผยต่อโลก และถูกกำหนดไว้ในโฟลเดอร์ rest
โปรดทราบว่า GraphQL สามารถติดตั้งบนแบ็คเอนด์และฐานข้อมูลประเภทใดก็ได้ โดยใช้ภาษาการเขียนโปรแกรมใดก็ได้ เทคโนโลยีที่ใช้ในที่นี้ได้รับการคัดเลือกมาเพื่อให้ง่ายต่อการอ่าน

เป้าหมายของเราคือการสร้าง /graphql
endpoint โดยไม่ต้องลบ REST endpoint ตำแหน่งข้อมูล GraphQL จะกระทบกับ ORM ของฐานข้อมูลโดยตรงเพื่อดึงข้อมูล เพื่อให้เป็นอิสระจากตรรกะ REST โดยสิ้นเชิง
ประเภท
ตัวแบบข้อมูลจะแสดงใน GraphQL ตาม ประเภท ซึ่งถูกพิมพ์อย่างชัดเจน ควรมีการจับคู่แบบ 1 ต่อ 1 ระหว่างแบบจำลองของคุณและประเภท GraphQL ประเภท User
ของเราจะเป็น:
type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }
แบบสอบถาม
การ สืบค้น ข้อมูลกำหนดว่าคุณสามารถเรียกใช้การสืบค้นใดบน GraphQL API ของคุณ ตามแบบแผน ควรมี RootQuery
ซึ่งมีการสืบค้นข้อมูลที่มีอยู่ทั้งหมด ฉันยังชี้ให้เห็น REST ที่เทียบเท่ากันของแต่ละแบบสอบถาม:
type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }
การกลายพันธุ์
หากการสืบค้นเป็นคำขอ GET
การ กลายพันธุ์ สามารถเห็นได้ว่าเป็น POST
/ PATCH
/ PUT
/ DELETE
(แม้ว่าจริงๆ แล้วจะเป็นการสืบค้นเวอร์ชันที่ซิงโครไนซ์)
ตามแบบแผน เราใส่การกลายพันธุ์ทั้งหมดของเราใน RootMutation
:
type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }
โปรดทราบว่าเราได้แนะนำประเภทใหม่ที่นี่ เรียกว่า UserInput
, ProjectInput
และ TaskInput
นี่เป็นแนวทางปฏิบัติทั่วไปของ REST เช่นกัน เพื่อสร้างโมเดลข้อมูลอินพุตสำหรับการสร้างและอัปเดตทรัพยากร ที่นี่ ประเภท UserInput
ของเราคือประเภท User
ของเราโดยไม่มีฟิลด์ id
และ projects
และสังเกตการ input
คำหลักแทน type
:
input UserInput { firstname: String lastname: String email: String }
สคีมา
ด้วยประเภท การสืบค้น และการกลายพันธุ์ เรากำหนด สคีมา GraphQL ซึ่งจุดปลาย GraphQL จะเปิดเผยต่อโลก:
schema { query: RootQuery mutation: RootMutation }
สคีมานี้ได้รับการพิมพ์อย่างเข้มงวดและเป็นสิ่งที่ช่วยให้เรามีการเติมข้อความอัตโนมัติที่มีประโยชน์เหล่านั้นใน GraphiQL
ตัวแก้ไข
ตอนนี้เรามีสคีมาสาธารณะแล้ว ก็ถึงเวลาบอก GraphQL ว่าต้องทำอย่างไรเมื่อมีการร้องขอการสืบค้น/การกลายพันธุ์แต่ละครั้ง นัก แก้ ปัญหาทำงานหนัก พวกเขาสามารถ ตัวอย่างเช่น:
- เข้าถึงจุดสิ้นสุด REST ภายใน
- โทรหาไมโครเซอร์วิส
- กดชั้นฐานข้อมูลเพื่อทำการดำเนินการ CRUD
เรากำลังเลือกตัวเลือกที่สามในแอปตัวอย่างของเรา มาดูไฟล์ตัวแก้ไขของเรากัน:
const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }
ซึ่งหมายความว่าหากมีการร้องขอการสืบค้น user(id: ID!)
บน GraphQL เราจะส่งคืน User.findById()
ซึ่งเป็นฟังก์ชัน Sequelize ORM จากฐานข้อมูล
แล้วการเข้าร่วมกับรุ่นอื่นๆ ในคำขอล่ะ? เราต้องกำหนดตัวแก้ไขเพิ่มเติม:
User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */
ดังนั้นเมื่อเราขอฟิลด์ projects
ในประเภท User
ใน GraphQL การรวมนี้จะถูกผนวกเข้ากับแบบสอบถามฐานข้อมูล
และสุดท้าย ตัวแก้ไขสำหรับการกลายพันธุ์:
RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }
คุณสามารถเล่นกับสิ่งนี้ได้ที่นี่ เพื่อรักษาข้อมูลบนเซิร์ฟเวอร์ให้สะอาด ฉันปิดใช้งานตัวแก้ไขสำหรับการกลายพันธุ์ ซึ่งหมายความว่าการกลายพันธุ์จะไม่สร้าง อัปเดต หรือลบการดำเนินการใดๆ ในฐานข้อมูล (และส่งคืน null
บนอินเทอร์เฟซ)
แบบสอบถาม
query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }
ผลลัพธ์
{ "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }
อาจใช้เวลาสักครู่ในการเขียนใหม่ทุกประเภท การสืบค้น และตัวแก้ไขสำหรับแอปที่มีอยู่ของคุณ อย่างไรก็ตาม มีเครื่องมือมากมายที่จะช่วยคุณ ตัวอย่างเช่น มีเครื่องมือที่แปลสคีมา SQL เป็นสคีมา GraphQL รวมถึงตัวแก้ไขด้วย!
รวมทุกอย่างไว้ด้วยกัน
ด้วยสคีมาและตัวแก้ไขที่กำหนดไว้อย่างดีเกี่ยวกับสิ่งที่ต้องทำในแต่ละเคียวรีของสคีมา เราสามารถติดตั้งจุดปลาย /graphql
บนแบ็คเอนด์ของเราได้:
// Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));
และเราสามารถมีอินเทอร์เฟซ GraphiQL ที่สวยงามในส่วนแบ็คเอนด์ของเราได้ หากต้องการส่งคำขอโดยไม่ใช้ GraphiQL เพียงคัดลอก URL ของคำขอแล้วเรียกใช้ด้วย cURL, AJAX หรือในเบราว์เซอร์โดยตรง แน่นอนว่ามีไคลเอ็นต์ GraphQL บางตัวที่จะช่วยคุณสร้างข้อความค้นหาเหล่านี้ ดูตัวอย่างด้านล่าง
อะไรต่อไป?
บทความนี้มีจุดมุ่งหมายเพื่อให้คุณได้ลิ้มรสว่า GraphQL หน้าตาเป็นอย่างไร และแสดงให้คุณเห็นว่าคุณสามารถลองใช้ GraphQL ได้โดยไม่ต้องทิ้งโครงสร้างพื้นฐาน REST ของคุณทิ้งไป วิธีที่ดีที่สุดที่จะทราบว่า GraphQL เหมาะสมกับความต้องการของคุณหรือไม่คือลองใช้งานด้วยตัวเอง ฉันหวังว่าบทความนี้จะทำให้คุณดำน้ำ
มีคุณลักษณะมากมายที่เรายังไม่ได้กล่าวถึงในบทความนี้ เช่น การอัปเดตตามเวลาจริง การแบ่งกลุ่มฝั่งเซิร์ฟเวอร์ การตรวจสอบสิทธิ์ การอนุญาต การแคชฝั่งไคลเอ็นต์ การอัปโหลดไฟล์ เป็นต้น แหล่งข้อมูลที่ยอดเยี่ยมสำหรับการเรียนรู้เกี่ยวกับคุณลักษณะเหล่านี้ คือ How to GraphQL
ด้านล่างนี้คือแหล่งข้อมูลที่มีประโยชน์อื่นๆ:
เครื่องมือฝั่งเซิร์ฟเวอร์ | คำอธิบาย |
---|---|
graphql-js | การใช้งานอ้างอิงของ GraphQL คุณสามารถใช้กับ express-graphql เพื่อสร้างเซิร์ฟเวอร์ |
graphql-server | เซิร์ฟเวอร์ GraphQL แบบครบวงจรที่สร้างโดยทีม Apollo |
การใช้งานสำหรับแพลตฟอร์มอื่นๆ | Ruby, PHP, ฯลฯ. |
เครื่องมือฝั่งไคลเอ็นต์ | คำอธิบาย |
---|---|
รีเลย์ | กรอบงานสำหรับเชื่อมต่อ React กับ GraphQL |
ลูกค้าอพอลโล | ไคลเอนต์ GraphQL ที่มีการผูกสำหรับ React, Angular 2 และเฟรมเวิร์กส่วนหน้าอื่น ๆ |
โดยสรุป ฉันเชื่อว่า GraphQL เป็นมากกว่าโฆษณา พรุ่งนี้จะไม่แทนที่ REST แต่เสนอวิธีแก้ปัญหาที่มีประสิทธิภาพสำหรับปัญหาที่แท้จริง มันค่อนข้างใหม่และแนวทางปฏิบัติที่ดีที่สุดยังคงพัฒนาอยู่ แต่เป็นเทคโนโลยีที่เราจะได้ยินในอีกไม่กี่ปีข้างหน้าอย่างแน่นอน