การแมป GraphQL Schema สำหรับเว็บไซต์ ธีม หรือปลั๊กอิน WordPress ของคุณ
สมมติว่าคุณตัดสินใจเริ่มใช้ GraphQL สำหรับเว็บไซต์ WordPress ที่มีอยู่ของคุณ ดีมาก! ไม่ว่าจะใช้สำหรับฟังก์ชันใหม่หรือที่มีอยู่แล้ว GraphQL จะต้องทำงานร่วมกับชั้นข้อมูลพื้นฐาน ดังนั้นคุณจำเป็นต้องแมปโมเดลข้อมูลของแอปพลิเคชัน (ไม่ว่าจะเป็น PHP code ที่กำหนดเองในเว็บไซต์ WordPress ธีม หรือปลั๊กอินของคุณ) ลงใน GraphQL schema
ควรทำการแมปอย่างไร? จำเป็นต้องทำทั้งหมดในคราวเดียวหรือไม่? ควรเป็นสำเนาที่เหมือนกันทุกประการของโมเดลข้อมูลที่มีอยู่หรือไม่? แล้วการปรับชื่อที่ไม่เหมาะสมระหว่างกระบวนการล่ะ? และเกี่ยวกับหนี้ทางเทคนิค ควรคงไว้หรือจัดการกับมัน?
มาสำรวจกลยุทธ์บางอย่างสำหรับการแมปโมเดลข้อมูลของแอปพลิเคชัน WordPress ที่มีอยู่ลงใน GraphQL schema กัน
แมป schema ตามจังหวะของคุณเอง
การเพิ่ม GraphQL ลงในแอปพลิเคชันไม่ใช่การเลือกทั้งหมดหรือไม่เลือกเลย แอปพลิเคชันเดียวกันอาจทำงานด้วย API หลายตัวพร้อมกัน ซึ่งในกรณีนั้น GraphQL จะอยู่ร่วมกับ API อื่นๆ เท่าที่จำเป็น ตัวอย่างเช่น เราสามารถคงฟังก์ชันที่มีอยู่ไว้โดยใช้ REST และนำ GraphQL มาใช้สำหรับฟังก์ชันใหม่เท่านั้น
หากคุณต้องการย้ายไปใช้ GraphQL อย่างสมบูรณ์ ก็ไม่จำเป็นต้องทำทั้งหมดในคราวเดียว ฟังก์ชันที่มีอยู่สามารถค่อยๆ ย้ายไปยัง GraphQL อย่างช้าแต่แน่นอน จนกว่าในวันหนึ่ง GraphQL จะกลายเป็น API เดียวในแอปพลิเคชัน
ดังนั้น แม้ว่าคุณจะสามารถสร้าง GraphQL schema ที่สมบูรณ์ในวันแรก แต่ก็ไม่จำเป็นต้องทำ ในช่วงเวลาใดก็ตาม เพียงแค่เอนทิตีที่จำเป็นสำหรับฟังก์ชันนั้นๆ ต้องปรากฏบน schema (ผ่าน types, fields และ interfaces ของพวกมัน) คุณสามารถแมปพวกมันตามเวลาที่ผ่านไป ในลักษณะที่ค่อยเป็นค่อยไป
อย่าให้ interface แบกภาระของการ implementation
GraphQL server จะนำ logic ในการเข้าถึงข้อมูลของแอปพลิเคชันไปใช้ โดยจะทำสิ่งนี้โดยการเรียกฟังก์ชันจาก WordPress เช่น การเรียก get_posts เพื่อดึงข้อมูลโพสต์ ในชั้นนี้จะมี PHP code เพื่อตอบสนอง resolvers
อย่างไรก็ตาม GraphQL schema คือ interface: มันประกาศสัญญาในการเข้าถึงข้อมูลใน API โดยไม่สนใจรายละเอียดการ implementation ไม่รู้อะไรเกี่ยวกับ WordPress หรือฟังก์ชัน get_posts ตาราง DB wp_posts หรือ SQL queries
ดังนั้นเราควรหลีกเลี่ยงการรั่วไหลของข้อมูลระหว่างชั้นต่างๆ ให้มากที่สุดเท่าที่จะทำได้
สิ่งนี้มีความสำคัญ เพราะโมเดลข้อมูลมักจะถูกบิดเบือนโดย implementation ของมัน WordPress มีตัวอย่างที่ชัดเจนด้วย "attachment" CPT เพื่อแทนไฟล์มีเดีย เช่น รูปภาพ
เนื่องจากเป็น Custom Post Type รูปภาพจึงถูกจัดการเป็นโพสต์ ดังนั้น เราอาจถูกดึงดูดให้แทนไฟล์มีเดียโดยใช้ type Post ซึ่งมี fields เหล่านี้:
type Post {
id: ID!
title: String
content: String
excerpt: String
}แต่นี่อาจไม่เหมาะสมสำหรับแอปพลิเคชัน ความหมายของ field "content" นั้นชัดเจนสำหรับโพสต์ แต่ไม่ชัดเจนสำหรับรูปภาพ โดยส่วนใหญ่แล้วมันไม่ควรอยู่ที่นั่น
รูปภาพถูกสร้างแบบจำลองเป็น CPT ใน WordPress เพราะมันสะดวก ทำให้สามารถนำ logic ที่มีอยู่มาใช้ซ้ำได้ และสามารถจัดเก็บในตาราง wp_posts ที่มีอยู่แล้วได้
อย่างไรก็ตาม ความสะดวกไม่ได้หมายความว่าเหมาะสม และอาจนำไปสู่หนี้ทางเทคนิคในที่สุด (นั่นคือ code ที่มีข้อบกพร่องซึ่งไม่สามารถแก้ไขได้โดยไม่เกิด breaking change ดังนั้นจึงถูกเก็บไว้ในแอปพลิเคชันนานกว่าที่ควร)
เราไม่ต้องการเก็บหนี้ทางเทคนิคไว้ในแอปพลิเคชันของเรามากที่สุดเท่าที่จะทำได้ เมื่อมีโอกาส เราควรแก้ไขมัน การแมปโมเดลข้อมูลลงใน GraphQL schema เป็นโอกาสเช่นนั้น ทำให้เราสามารถแก้ไขปัญหาในชั้น interface ของข้อมูลได้
(หนี้ทางเทคนิคจะยังคงมีอยู่ในระดับแอปพลิเคชันอยู่ดี ดังนั้นเราจึงไม่ได้แก้ไขปัญหาอย่างสมบูรณ์ แต่ลดทอนมันภายในขีดความสามารถของเรา)
มาทำให้แนวคิดนี้เป็นรูปธรรมกัน แทนที่จะใช้ type Post แทนไฟล์มีเดีย การมี type Media ที่มีเฉพาะ properties ที่สมเหตุสมผลสำหรับเอนทิตีรูปภาพนั้นสมเหตุสมผลกว่า:
type Media {
id: ID!
src: String!
width: Int
height: Int
}ภายใต้ประทุน ในระดับ implementation field resolver จะยังคงเรียกใช้ฟังก์ชัน get_posts เพื่อ resolve รายการของ type Media แต่นั่นไม่ใช่สิ่งที่ GraphQL schema ต้องกังวล
แยก GraphQL schema ออกจาก DB diagram
WordPress ถูก implement บนพื้นฐาน DB entity-relationship diagram นี้:

เราต้องให้ GraphQL schema ยึดตาม DB diagram แต่เราไม่ควรพยายามสร้าง replica แบบ 1 ต่อ 1 เพราะทั้ง GraphQL schema และ DB diagram ต่างถูกสร้างขึ้นด้วยเงื่อนไขหรือข้อจำกัดบางอย่าง ซึ่งจะไม่นำไปใช้กับอีกฝ่าย
ส่วนก่อนหน้าแสดงตัวอย่าง ที่ตาราง wp_posts จัดเก็บข้อมูลสำหรับ image CPT แต่ใน GraphQL จะมี types ที่แตกต่างกันสองแบบ คือ Post และ Media
ลองพิจารณาอีกตัวอย่างหนึ่ง: categories ใน WordPress โพสต์สามารถมี category (หรือมากกว่า) และ CPT ใดก็ตามยังสามารถสร้าง category ของตัวเองได้ ตัวอย่างเช่น CPT ที่เรียกว่า "event" จะมี "event_category"
ทั้ง post categories และ event categories ถูกจัดเก็บในตาราง wp_terms สิ่งนี้ทำให้ WordPress สามารถดึงแถวจาก category type ใดก็ตามได้ง่ายขึ้นเมื่อเรียกใช้ SQL query
ดังนั้น เราอาจถูกดึงดูดให้แมป categories ผ่าน type Category ที่ถูกอ้างอิงโดยทั้งโพสต์และ events:
type Category {
id: ID!
name: String!
}
type Post {
categories: [Category]!
}
type Event {
categories: [Category]!
}อย่างไรก็ตาม โพสต์จะมี post categories เสมอ และ event จะมี event categories เสมอ ข้อมูลสำหรับ category types สองประเภทนี้อาจถูกจัดเก็บในตาราง DB เดียวกัน แต่จะไม่ปะปนกันในระดับแอปพลิเคชัน Post category และ event category เป็นสองเอนทิตีที่แตกต่างกัน
GraphQL มี ระบบ type แบบ static เพื่อใช้ GraphQL ให้เกิดประโยชน์สูงสุด เอนทิตีที่แตกต่างกันในระดับแอปพลิเคชันต้องถูกสร้างแบบจำลองโดยใช้ types ที่แตกต่างกันใน GraphQL schema
ในกรณีนี้ เมื่อแมป categories ลงใน GraphQL schema เราควรสร้าง type ที่แตกต่างกันสำหรับแต่ละประเภท: PostCategory และ EventCategory จากนั้น type Post จะอ้างอิงเฉพาะ PostCategory และ type Event จะอ้างอิงเฉพาะ EventCategory:
type PostCategory {
id: ID!
name: String!
}
type Post {
categories: [PostCategory]!
}
type EventCategory {
id: ID!
name: String!
}
type Event {
categories: [EventCategory]!
}หากเรายังต้องการมีเอนทิตีใน schema ที่ครอบคลุม categories ทั้งหมด สามารถทำได้ผ่าน interface Category:
interface Category {
name: String!
}
type PostCategory implements Category {
id: ID!
name: String!
}
type EventCategory implements Category {
id: ID!
name: String!
}วิธีนี้ ผู้ใช้ที่เข้าถึง API จะเข้าใจอย่างชัดเจนว่าข้อมูลใดจะถูกดึงมา โดยไม่ขึ้นอยู่กับวิธีที่มันถูกแมปใน DB diagram และวิธีที่มันถูกจัดเก็บใน DB
เมื่อเรามี GraphQL schema ขั้นสุดท้ายแล้ว เราจะเห็นว่ารูปร่างของมันค่อนข้างคล้ายคลึงกับ WordPress DB diagram แต่ก็แตกต่างอย่างชัดเจน:

ปรับชื่อของ fields ตาม static typing
Fields ควรเคารพชื่อเดียวกับที่มีในแอปพลิเคชันให้มากที่สุดเท่าที่จะทำได้
ตัวอย่างเช่น เราสามารถสร้างโพสต์ด้วยฟังก์ชัน wp_insert_post และโพสต์มี properties "title" และ "content" ชื่อเหล่านี้ก็เหมาะกับ GraphQL schema เช่นกัน (แม้ว่าอาจต้องการการปรับเปลี่ยนเล็กน้อย) ดังนั้นเราควรเก็บไว้:
type MutationRoot {
insertPost(title: String, content: String): Post
}
type Post {
id: ID!
title: String
content: String
}แต่นั่นไม่จำเป็นต้องเป็นเช่นนั้นเสมอไป อย่างที่เราเห็นก่อนหน้านี้ custom posts ต้องถูกแยกออกเป็นเอนทิตีของตัวเอง ดังนั้น ในขณะที่ฟังก์ชัน get_posts ดึงรายการของ CPT ใดก็ตาม field ที่เทียบเท่า posts ใน root type ของ schema จะดึงเฉพาะเอนทิตีของ type Post เท่านั้น แต่ไม่รวม Page (ซึ่งก็เป็น CPT เช่นกัน):
type QueryRoot {
posts: [Post]!
}แล้วเราจะได้รายการของโพสต์และหน้าทั้งหมดได้อย่างไร? ผ่าน field อีกตัวหนึ่ง customPosts ซึ่งดึงเอนทิตีของ CPT ใดก็ตามที่แมปภายใต้ union type CustomPostUnion:
union CustomPostUnion = Post | Page
type QueryRoot {
customPosts: [CustomPostUnion]!
}บทเรียนที่สำคัญคือ: การตั้งชื่อที่เราเลือกสำหรับ GraphQL schema ต้องถูกปรับให้เหมาะกับ type ของเอนทิตีที่ดึงมา และเนื่องจาก strong types ของ GraphQL type ดังกล่าวอาจแตกต่างกันที่ชั้นแอปพลิเคชันและ API
ในกรณีนี้ ในขณะที่ใน WordPress "post" อาจหมายถึง "custom post type" ใดก็ตาม ใน GraphQL "post" จำเป็นต้องเป็น Post หาก field ดึง custom posts field ใน GraphQL schema ต้องตั้งชื่อว่า customPosts ไม่ใช่ posts ในทำนองเดียวกัน หาก input รับ ID สำหรับ custom post ต้องเรียกว่า customPostID ไม่ใช่ postID

บทเรียนนี้ถูกนำไปใช้กับ comments เช่นกัน comment สามารถเพิ่มลงใน CPT ใดก็ตาม ไม่ใช่แค่โพสต์เท่านั้น ดังนั้น type Comment ต้องทำให้สิ่งนี้ชัดเจน โดยมี field customPost (ไม่ใช่ post):
type Comment {
id: ID!
customPost: CustomPostUnion!
}แปลงค่าสตริงที่กำหนดไว้ล่วงหน้าเป็น enums โดยใช้ตัวพิมพ์ใหญ่เมื่อเป็นไปได้
Enumeration types ตามแบบแผนจะถูกกำหนดเป็นตัวพิมพ์ใหญ่ ตัวอย่างเช่น เอกสารจาก graphql.org มีตัวอย่างนี้:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}เมื่อใดก็ตามที่เราต้องสร้าง enum type ใหม่ เราควรใช้ตัวพิมพ์ใหญ่สำหรับค่าคงที่ที่กำหนดไว้ อย่างไรก็ตาม เมื่อทำการ migrate โมเดลข้อมูลจากแอปพลิเคชัน เราอาจพบชุดค่าที่กำหนดไว้ล่วงหน้าบางชุดที่เราสามารถแมปผ่าน enum ได้ แต่ค่าเหล่านั้นเป็นสตริงตัวพิมพ์เล็ก
เพื่อยกตัวอย่าง โพสต์ใน WordPress มี property "status" ซึ่งมีค่าใดค่าหนึ่งต่อไปนี้:
"publish""pending""draft""trash"
เมื่อแมป property นี้บน schema field Post.status อาจส่งคืน String แบบนี้:
type Post {
status: String!
}อย่างไรก็ตาม เนื่องจาก status จำเป็นต้องเป็นค่าใดค่าหนึ่งจากค่าที่กำหนดไว้เหล่านั้น และไม่มีค่าอื่น เราจึงควรแมปเป็น enum:
enum Status {
PUBLISH
DRAFT
PENDING
TRASH
}
type Post {
status: Status!
}ตอนนี้เราอาจมีปัญหา: enum PUBLISH จะถูกแปลงเป็นค่าสตริง "PUBLISH" ในแอปพลิเคชัน ไม่ใช่ "publish"
การใช้ค่าตัวพิมพ์ใหญ่แทนที่จะเป็นตัวพิมพ์เล็กตามที่คาดหวัง อาจทำให้ logic ในแอปพลิเคชันหยุดชะงัก จริงๆ แล้ว การรัน code ต่อไปนี้ใน WordPress จะไม่ทำงาน:
// This will retrieve all posts, not only the published ones
$published_posts = get_posts([
"post_status" => "PUBLISH",
]);ในกรณีนี้ เราสามารถพิจารณาแลกเปลี่ยนระหว่างแบบแผนและความสะดวก โดยยังคงใช้ enum เพื่อแมปค่าคงที่ แต่ใช้ตัวพิมพ์เล็ก:
enum Status {
publish
draft
pending
trash
}กล่าวอีกนัยหนึ่ง เราสามารถหาจุดกึ่งกลางระหว่างความเหมาะสมและความปฏิบัติได้ เราควรใช้ best practices เมื่อสร้าง GraphQL schema แต่อนุญาตให้ตัวเองเบี่ยงเบนจากมันได้เมื่อมีเหตุผลที่สมเหตุสมผล