Khái niệm, ý tưởng, chiến lược
Khái niệm, ý tưởng, chiến lượcÁnh xạ Schema GraphQL cho trang web, chủ đề hoặc plugin WordPress của bạn

Ánh xạ Schema GraphQL cho trang web, chủ đề hoặc plugin WordPress của bạn

Vậy là bạn đã quyết định bắt đầu sử dụng GraphQL cho trang WordPress hiện có của mình. Tuyệt vời! Dù được sử dụng cho chức năng mới hay chức năng hiện có, GraphQL sẽ cần tương tác với lớp dữ liệu bên dưới, và để làm được điều đó, bạn cần ánh xạ mô hình dữ liệu của ứng dụng (dù là code PHP tùy chỉnh trong trang WordPress, chủ đề, hay plugin của bạn) vào schema GraphQL.

Việc ánh xạ nên được thực hiện như thế nào? Có cần thực hiện tất cả cùng một lúc không? Có nên là bản sao chính xác của mô hình dữ liệu hiện có không? Còn việc điều chỉnh một tên không phù hợp trong quá trình này thì sao? Và về nợ kỹ thuật, nên giữ nguyên hay xử lý nó?

Hãy cùng khám phá một số chiến lược để ánh xạ mô hình dữ liệu của một ứng dụng WordPress hiện có vào schema GraphQL.

Ánh xạ schema theo tốc độ của bạn

Thêm GraphQL vào một ứng dụng không phải là tất cả hoặc không có gì. Cùng một ứng dụng có thể được vận hành bởi nhiều API đồng thời, trong trường hợp đó GraphQL sẽ tồn tại cùng với các API khác, miễn là cần thiết. Ví dụ, chúng ta có thể giữ nguyên chức năng hiện có được vận hành bởi REST, và chỉ áp dụng GraphQL cho tất cả chức năng mới.

Nếu bạn muốn thực hiện việc di chuyển hoàn toàn sang GraphQL, bạn không cần phải làm tất cả cùng một lúc. Chức năng hiện có có thể được di chuyển dần dần sang GraphQL, cho đến một ngày GraphQL trở thành API duy nhất trong ứng dụng.

Do đó, dù bạn có thể tạo schema GraphQL đầy đủ ngay từ ngày đầu tiên, bạn không bắt buộc phải làm vậy: Tại bất kỳ thời điểm nào, chỉ những thực thể cần thiết cho chức năng mới phải có mặt trong schema (thông qua các kiểu, trường và giao diện của chúng). Bạn có thể ánh xạ chúng dần dần theo thời gian, theo cách tiến hành từng bước.

Đừng để giao diện gánh chịu gánh nặng của việc triển khai

Máy chủ GraphQL sẽ triển khai logic để truy cập dữ liệu của ứng dụng. Nó sẽ thực hiện điều này bằng cách gọi chức năng từ WordPress, chẳng hạn như gọi get_posts để lấy dữ liệu bài viết. Ở lớp này, có code PHP để thỏa mãn các resolver.

Tuy nhiên, schema GraphQL là một giao diện: Nó khai báo các hợp đồng để truy cập dữ liệu trong API. Nó không quan tâm đến các chi tiết triển khai: Nó không biết gì về WordPress, hay về hàm get_posts, bảng DB wp_posts hay các queries SQL.

Vì vậy, chúng ta nên tránh rò rỉ thông tin giữa các lớp càng nhiều càng tốt.

Điều này quan trọng, vì mô hình dữ liệu thường sẽ bị ảnh hưởng bởi việc triển khai của nó. WordPress cung cấp một ví dụ rõ ràng về điều này với CPT "attachment", để biểu diễn các tệp phương tiện như hình ảnh.

Vì đây là một Custom Post Type, một hình ảnh được coi như một bài viết. Sau đó, chúng ta có thể bị cám dỗ để biểu diễn các tệp phương tiện bằng kiểu Post, chứa các trường này:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
}

Nhưng điều này có thể không phù hợp với ứng dụng. Ý nghĩa của trường "content" rõ ràng đối với một bài viết, nhưng không rõ ràng đối với một hình ảnh. Nhiều khả năng, nó không nên thuộc về đó.

Một hình ảnh được mô hình hóa như một CPT trong WordPress vì nó tiện lợi, để nó có thể tái sử dụng logic hiện có và có thể được lưu trữ trong bảng wp_posts hiện có.

Tuy nhiên, tiện lợi không có nghĩa là phù hợp, và cuối cùng có thể dẫn đến nợ kỹ thuật (tức là code không đủ tốt không thể được sửa chữa mà không tạo ra thay đổi phá vỡ, vì vậy nó được giữ trong ứng dụng lâu hơn mức cần thiết).

Càng nhiều càng tốt, chúng ta không muốn giữ nợ kỹ thuật trong ứng dụng của mình. Bất cứ khi nào có cơ hội, chúng ta nên sửa chữa nó. Việc ánh xạ mô hình dữ liệu sang schema GraphQL cung cấp một cơ hội như vậy, cho phép chúng ta khắc phục vấn đề ở lớp giao diện dữ liệu.

(Nợ kỹ thuật vẫn sẽ tồn tại ở cấp độ ứng dụng, vì vậy chúng ta không hoàn toàn giải quyết được vấn đề, nhưng giảm thiểu nó trong khả năng của mình.)

Hãy áp dụng ý tưởng này vào thực tế. Thay vì có kiểu Post đại diện cho các tệp phương tiện, sẽ hợp lý hơn khi có kiểu Media, chỉ chứa những thuộc tính có ý nghĩa đối với một thực thể hình ảnh:

type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Bên dưới, ở cấp độ triển khai, field resolver vẫn sẽ thực thi hàm get_posts để phân giải các mục nhập của kiểu Media, nhưng điều đó không liên quan đến schema GraphQL.

Tách biệt schema GraphQL khỏi sơ đồ DB

WordPress được triển khai trên sơ đồ quan hệ thực thể DB này:

Sơ đồ quan hệ thực thể DB trong WordPress

Chúng ta phải có schema GraphQL dựa trên sơ đồ DB, nhưng chúng ta không nên cố gắng tạo ra một bản sao 1 đến 1. Đó là vì cả schema GraphQL và sơ đồ DB đều được xây dựng với những điều kiện tiên quyết hoặc giới hạn nhất định, sẽ không áp dụng cho cái kia.

Phần trước minh họa một ví dụ, trong đó bảng wp_posts lưu trữ dữ liệu cho CPT hình ảnh, nhưng trong GraphQL sẽ có hai kiểu riêng biệt, PostMedia.

Hãy xem xét một ví dụ khác: danh mục. Trong WordPress, một bài viết có thể có một danh mục (hoặc nhiều hơn), và bất kỳ CPT nào cũng có thể tạo danh mục riêng của nó. Ví dụ, một CPT có tên "event" sẽ có "event_category".

Cả danh mục bài viết và danh mục sự kiện đều được lưu trữ trong bảng wp_terms. Điều này giúp WordPress dễ dàng lấy các hàng từ loại danh mục này hay loại kia, khi thực thi queries SQL.

Do đó, chúng ta có thể bị cám dỗ để ánh xạ các danh mục thông qua kiểu Category, được tham chiếu bởi cả bài viết lẫn sự kiện:

type Category {
  id: ID!
  name: String!
}
 
type Post {
  categories: [Category]!
}
 
type Event {
  categories: [Category]!
}

Tuy nhiên, một bài viết sẽ luôn chứa danh mục bài viết, và một sự kiện sẽ luôn chứa danh mục sự kiện. Dữ liệu của hai loại danh mục này có thể được lưu trữ trong cùng một bảng DB, nhưng sẽ không bị pha trộn ở cấp độ ứng dụng. Danh mục bài viết và danh mục sự kiện là hai thực thể khác biệt.

GraphQL có hệ thống kiểu tĩnh. Để tận dụng tối đa GraphQL, các thực thể khác nhau ở cấp độ ứng dụng phải được mô hình hóa bằng các kiểu khác nhau trong schema GraphQL.

Trong trường hợp này, khi ánh xạ danh mục vào schema GraphQL, chúng ta nên tạo một kiểu khác nhau cho mỗi loại: PostCategoryEventCategory. Sau đó, kiểu Post sẽ chỉ tham chiếu PostCategory, và kiểu Event sẽ chỉ tham chiếu EventCategory:

type PostCategory {
  id: ID!
  name: String!
}
 
type Post {
  categories: [PostCategory]!
}
 
type EventCategory {
  id: ID!
  name: String!
}
 
type Event {
  categories: [EventCategory]!
}

Nếu chúng ta vẫn muốn có một thực thể trong schema bao gồm tất cả danh mục, điều này có thể đạt được thông qua giao diện Category:

interface Category {
  name: String!
}
 
type PostCategory implements Category {
  id: ID!
  name: String!
}
 
type EventCategory implements Category {
  id: ID!
  name: String!
}

Theo cách này, người dùng truy cập API sẽ hiểu rõ ràng dữ liệu nào sẽ được truy xuất, bất kể nó được ánh xạ như thế nào trong sơ đồ DB và được lưu trữ như thế nào trong DB.

Sau khi có schema GraphQL cuối cùng, chúng ta có thể nhận thấy rằng hình dạng của nó sẽ phần nào giống sơ đồ DB WordPress, nhưng rõ ràng là khác biệt với nó:

Schema GraphQL

Điều chỉnh cách đặt tên các trường, tuân theo kiểu tĩnh

Các trường nên, càng nhiều càng tốt, tôn trọng cách đặt tên giống như chúng có trong ứng dụng.

Ví dụ, chúng ta có thể tạo một bài viết với hàm wp_insert_post, và bài viết có các thuộc tính "title" và "content". Những tên này cũng tốt cho schema GraphQL (dù chúng có thể cần một số sửa đổi nhỏ), vì vậy chúng ta nên giữ chúng:

type MutationRoot {
  insertPost(title: String, content: String): Post
}
 
type Post {
  id: ID!
  title: String
  content: String
}

Nhưng điều đó không nhất thiết luôn như vậy. Như chúng ta đã thấy trước đó, các bài viết tùy chỉnh phải được tách biệt vào các thực thể riêng của chúng. Sau đó, trong khi hàm get_posts lấy danh sách bất kỳ CPT nào, một trường tương đương posts trong kiểu gốc của schema sẽ chỉ lấy các thực thể kiểu Post, chứ không phải Page (cũng là một CPT):

type QueryRoot {
  posts: [Post]!
}

Vậy làm thế nào để chúng ta lấy danh sách tất cả bài viết và trang? Thông qua một trường khác, customPosts, lấy các thực thể của bất kỳ CPT nào được ánh xạ dưới kiểu union CustomPostUnion:

union CustomPostUnion = Post | Page
 
type QueryRoot {
  customPosts: [CustomPostUnion]!
}

Bài học quan trọng là: Cách đặt tên chúng ta chọn cho schema GraphQL phải được điều chỉnh theo kiểu của thực thể được truy xuất. Và do các kiểu mạnh của GraphQL, kiểu đó có thể khác nhau ở lớp ứng dụng và lớp API.

Trong trường hợp này, trong khi trong WordPress một "post" có thể có nghĩa là bất kỳ "custom post type" nào, trong GraphQL một "post" nhất thiết phải là Post. Nếu một trường lấy các bài viết tùy chỉnh, thì trường trong schema GraphQL phải được đặt tên là customPosts, không phải posts. Tương tự, nếu một đầu vào nhận ID cho một bài viết tùy chỉnh, nó phải được gọi là customPostID, không phải postID.

Ánh xạ cho trường customPosts

Bài học này được áp dụng cho các bình luận, ví dụ. Một bình luận có thể được thêm vào bất kỳ CPT nào, không chỉ bài viết. Do đó, kiểu Comment phải làm rõ điều này, bằng cách chứa trường customPost (và không phải post):

type Comment {
  id: ID!
  customPost: CustomPostUnion!
}

Chuyển đổi các giá trị chuỗi được định sẵn thành enum, sử dụng chữ hoa nếu có thể

Các kiểu liệt kê, theo quy ước, được định nghĩa bằng chữ hoa. Ví dụ, tài liệu từ graphql.org cung cấp ví dụ này:

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

Bất cứ khi nào chúng ta cần tạo một kiểu enum mới, chúng ta nên sử dụng chữ hoa cho các hằng số được định nghĩa của nó. Tuy nhiên, khi di chuyển mô hình dữ liệu từ ứng dụng, chúng ta có thể gặp một số tập hợp giá trị được định sẵn mà chúng ta có thể ánh xạ thông qua enum, nhưng giá trị của chúng là chuỗi chữ thường.

Để đưa ra ví dụ, các bài viết trong WordPress có thuộc tính "status", chứa một trong các giá trị sau:

  • "publish"
  • "pending"
  • "draft"
  • "trash"

Khi ánh xạ thuộc tính này vào schema, trường Post.status có thể trả về một String, như thế này:

type Post {
  status: String!
}

Tuy nhiên, vì trạng thái nhất thiết sẽ là một trong những giá trị được định sẵn đó, và không có giá trị nào khác, chúng ta thích ánh xạ nó dưới dạng enum hơn:

enum Status {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}
 
type Post {
  status: Status!
}

Bây giờ, chúng ta có thể gặp một vấn đề: Enum PUBLISH sẽ được chuyển đổi thành giá trị chuỗi "PUBLISH" trong ứng dụng, không phải "publish".

Sử dụng giá trị chữ hoa, thay vì giá trị chữ thường dự kiến, logic trong ứng dụng có thể bị gián đoạn. Thật vậy, việc thực thi đoạn code sau trong WordPress không hoạt động:

// Điều này sẽ lấy tất cả bài viết, không chỉ những bài đã xuất bản
$published_posts = get_posts([
  "post_status" => "PUBLISH",
]);

Trong trường hợp này, chúng ta có thể xem xét đánh đổi quy ước với sự tiện lợi, vẫn sử dụng enum để ánh xạ các hằng số, nhưng bằng chữ thường:

enum Status {
  publish
  draft
  pending
  trash
}

Nói cách khác, chúng ta có thể tìm ra điểm trung gian giữa việc đúng đắn và thực tế. Chúng ta nên sử dụng các thực hành tốt nhất khi xây dựng schema GraphQL, nhưng cho phép bản thân lệch khỏi chúng bất cứ khi nào điều đó có lý.