Khái niệm, ý tưởng, chiến lược
Khái niệm, ý tưởng, chiến lượcCách plugin ánh xạ mô hình dữ liệu WordPress vào schema GraphQL

Cách plugin ánh xạ mô hình dữ liệu WordPress vào schema GraphQL

Đây là cách Gato GraphQL đã ánh xạ mô hình dữ liệu WordPress thành một schema GraphQL tương ứng.

Mô hình dữ liệu WordPress

WordPress có các thực thể sau:

  • posts
  • pages
  • custom posts
  • các phần tử media
  • người dùng
  • vai trò người dùng
  • tags
  • categories
  • comments
  • blocks
  • thuộc tính meta
  • các thứ khác (tùy chọn, plugin, theme, v.v.)

Các thực thể này có thể có thứ bậc. Ví dụ, post, page và các phần tử media đều là custom post types, còn tags và categories đều là taxonomies.

Đây là sơ đồ cơ sở dữ liệu WordPress, cho thấy cách dữ liệu cho tất cả các thực thể được lưu trữ:

Sơ đồ cơ sở dữ liệu WordPress

Việc ánh xạ có phải là bản sao chính xác của sơ đồ cơ sở dữ liệu không?

Khi ánh xạ cơ sở dữ liệu WordPress thành một schema GraphQL, sơ đồ trên có được tôn trọng theo tỷ lệ 1-1 không?

Không, không phải vậy. Trong khi sơ đồ cơ sở dữ liệu là một triển khai thực tế, GraphQL là một giao diện để truy cập dữ liệu từ phía client. Hai thứ này có liên quan nhau, nhưng chúng có thể khác nhau. GraphQL không quan tâm đến cơ sở dữ liệu: nó không nghĩ theo các lệnh SQL, hay biết rằng có các bảng cơ sở dữ liệu tên là wp_postswp_users.

Vì vậy, chúng ta không cần lo lắng quá nhiều về sơ đồ cơ sở dữ liệu khi tạo schema GraphQL cho WordPress. Hơn thế nữa, chúng ta có thể tạo ra một schema GraphQL giúp khắc phục một số nợ kỹ thuật từ mô hình dữ liệu WordPress.

Ánh xạ mô hình dữ liệu WordPress thành schema GraphQL

Hãy thực hiện việc ánh xạ. Đầu tiên, chúng ta ánh xạ các thực thể gốc thành các kiểu, càng nhiều càng tốt. Từ danh sách các thực thể trong mô hình dữ liệu WordPress, chúng ta tạo ra các kiểu sau cho schema GraphQL:

  • Post
  • Page
  • Media
  • User
  • UserRole
  • PostTag
  • PostCategory
  • Comment

Sau đó, chúng ta thêm tất cả các trường dự kiến vào mỗi kiểu. Để biểu diễn schema, chúng ta có thể sử dụng SDL, hay Schema Definition Language. (Cái này chỉ dùng cho mục đích tài liệu; bản thân plugin không dùng SDL để mã hóa schema: tất cả đều là code PHP).

Đây là các trường (trong số nhiều trường khác) cho một Post:

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

Đây là các trường (trong số nhiều trường khác) cho một User:

type User {
  id: ID!
  name: String
  email: String!
}

Chúng ta cũng tạo các kết nối tương ứng, là các trường trả về một thực thể khác (thay vì một scalar, chẳng hạn như một số hoặc một chuỗi). Ví dụ, chúng ta biểu diễn một post có một tác giả, và một người dùng sở hữu các post:

type Post {
  author: User!
}
 
type User {
  posts: [Post]
}

Các trường và kết nối cũng có thể chấp nhận các đối số. Ví dụ, chúng ta cho phép Post.dateStr được định dạng, và User.posts lọc các mục, giới hạn số lượng và sắp xếp chúng:

type Post {
  dateStr(format: String): Date!
}
 
type User {
  posts(
    filter: RootPostsFilterInput
    pagination: PostPaginationInput
    sort: CustomPostSortInput
  ): [Post!]!
}
 
input RootPostsFilterInput {
  authorIDs: [ID!]
  authorSlug: String
  categoryIDs: [ID!]
  dateQuery: [DateQueryInput!]
  excludeAuthorIDs: [ID!]
  excludeIDs: [ID!]
  hasPassword: Boolean = false
  ids: [ID!]
  isSticky: Boolean
  metaQuery: [CustomPostMetaQueryInput!]
  password: String
  search: String
  status: [FilterCustomPostStatusEnum!]
  tagIDs: [ID!]
  tagSlugs: [String!]
}
 
input PostPaginationInput {
  limit: Int
  offset: Int
}
 
input CustomPostSortInput {
  by: CustomPostOrderByEnum
  order: OrderEnum
}
 
# ...

Chúng ta tiếp tục làm điều này cho tất cả các thực thể trong mô hình dữ liệu WordPress. Khi hoàn thành, chúng ta sẽ đến được schema GraphQL cho WordPress, có thể xem qua client Voyager (có sẵn dưới dạng "Interactive Schema" trong menu của plugin):

Schema GraphQL cho WordPress

Schema này có những điểm tương đồng với sơ đồ cơ sở dữ liệu WordPress, nhưng cũng có một số điểm khác biệt. Hãy phân tích chúng.

Các thao tác không có thực thể được ánh xạ thành các trường Root

Sơ đồ cơ sở dữ liệu WordPress biểu diễn cách dữ liệu được lưu trữ, vì vậy không có "điểm bắt đầu". Tuy nhiên, GraphQL là một giao diện để truy xuất dữ liệu, do đó phải có một giai đoạn ban đầu để thực thi query.

Giai đoạn ban đầu này là kiểu Root, hay chính xác hơn là các kiểu QueryRootMutationRoot (để xử lý queries và mutations, tương ứng).

Trong hai kiểu này, chúng ta ánh xạ tất cả các thao tác không phụ thuộc vào một thực thể, chẳng hạn như khi thực thi get_posts(), get_users() hoặc wp_signon():

type QueryRoot {
  posts: [Post]!
  users: [User]!
}
 
type MutationRoot {
  loginUser(
    usernameOrEmail: String!,
    password: String!
  ): User
}

Các trường không nhất thiết phải có cùng tên hoặc chữ ký với thao tác mà chúng biểu diễn. Ví dụ, việc gọi trường loginUser có thể được coi là phù hợp hơn signOn.

Nhóm các phần tử schema

Chúng ta có thể áp dụng các cải tiến để đơn giản hóa schema và làm cho nó hữu ích hơn. Ví dụ, một trường có thể nhận tất cả các đối số của nó thông qua một đối tượng input, có thể được tái sử dụng trên nhiều trường và giúp dễ dàng hơn khi trực quan hóa schema:

type MutationRoot {
  loginUser(input: LoginUserByInput!): User
}
 
input LoginUserByInput {
    usernameOrEmail: String!,
    password: String!
}

Ngoài ra, phản hồi từ một mutation có thể là một đối tượng "payload", ngoài việc trả về đối tượng bị ảnh hưởng, còn có thể bao gồm trạng thái của thao tác và các thông báo lỗi:

type MutationRoot {
  loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
 
type RootLoginUserMutationPayload {
  errors: [RootLoginUserMutationErrorPayloadUnion!]
  status: OperationStatusEnum!
  user: User
  userID: ID
}
 
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
  | InvalidUserEmailErrorPayload
  | InvalidUsernameErrorPayload
  | PasswordIsIncorrectErrorPayload
  | UserIsLoggedInErrorPayload

Tất cả mutations đều nằm dưới MutationRoot

Có những thao tác phụ thuộc vào một thực thể, chẳng hạn như wp_update_post(), được áp dụng trên một post nào đó. Mutation tương ứng trong schema GraphQL phải được thêm vào kiểu MutationRoot, vì đó là cách GraphQL hoạt động.

Khi đó, thao tác này được ánh xạ như sau:

type MutationRoot {
  updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
 
input RootUpdatePostFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  id: ID!
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Plugin này cũng hỗ trợ nested mutations, được cung cấp như một tính năng tùy chọn (vì đây không phải là hành vi GraphQL chuẩn). Khi đó, mutations cũng có thể được thêm vào bất kỳ kiểu nào, không chỉ MutationRoot. Trong trường hợp này, chúng ta có:

type Post {
  update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
 
input PostUpdateFilterInput {
  categoryIDs: [ID!]
  content: String
  featuredImageID: ID
  status: CustomPostStatusEnum
  tags: [String!]
  title: String
}

Hãy lưu ý sự khác biệt giữa các input RootUpdatePostFilterInputPostUpdateFilterInput (tức là giữa mutations từ root và nested mutations): cái trước có thuộc tính bắt buộc id để chỉ định post nào cần sửa đổi, nhưng cái sau thì không, vì nó không cần.

Xử lý custom posts

Không có kế thừa kiểu trong GraphQL. Do đó, chúng ta không thể có kiểu CustomPost, và khai báo rằng PostPage kế thừa từ nó.

GraphQL cung cấp hai tài nguyên để bù đắp cho sự thiếu hụt này: interfaces và union types.

Đối với cái đầu tiên, chúng ta tạo một interface CustomPost cho schema, khai báo tất cả các trường dự kiến từ một custom post, và định nghĩa các kiểu Post, PageGenericCustomPost (để biểu diễn tất cả các custom post types được định nghĩa bởi bất kỳ theme và plugin đã cài đặt nào) để triển khai interface:

interface CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Post implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Page implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type GenericCustomPost implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}

Đối với cái thứ hai, chúng ta tạo một kiểu CustomPostUnion cho schema trả về tất cả các custom post types:

union CustomPostUnion = Post | Page | GenericCustomPost

Và để các trường trả về kiểu này khi thích hợp:

type QueryRoot {
  customPost(id: ID): CustomPostUnion
  customPosts: [CustomPostUnion]!
}
 
type User {
  customPosts: [CustomPostUnion]
}
 
type Comment {
  customPost: CustomPostUnion!
}

Khi thực thi query, chúng ta có thể chọn các trường dựa trên kiểu thực tế, chẳng hạn như Post, hoặc dựa trên interface CustomPost:

{  
  customPosts {
    __typename
    ...on CustomPost {
      id
      title
      slug
      status
    }
    ...on Post {
      isSticky
      postFormat
    }
  }
}

Như có thể thấy, trong schema GraphQL chúng ta cần khẳng định rõ ràng khi nào chúng ta đang xử lý posts, và khi nào là custom posts, vì chúng không giống nhau! Việc gọi hai thứ này có thể hoán đổi cho nhau là nợ kỹ thuật của WordPress, mà plugin cố gắng khắc phục bất cứ khi nào có thể.

Vì lý do này, một custom post luôn được gọi là CustomPost chứ không phải Post, một trường xử lý custom posts luôn được gọi là customPosts chứ không phải posts, và một đối số trường nhận ID cho một custom post được gọi là customPostID chứ không phải postID (mặc dù đó là cách nó được gọi trong hàm WordPress được ánh xạ).

Khi đó, kỳ vọng luôn rõ ràng:

  • Trường User.customPosts có thể trả về một danh sách bất kỳ custom post nào, bao gồm posts và pages, còn User.posts chỉ trả về posts
  • Trường Root.setFeaturedImageOnCustomPost có thể thêm một hình ảnh nổi bật vào bất kỳ custom post nào, đó là lý do tại sao nó không được gọi là setFeaturedImageOnPost

Không nhóm tags (và categories) dưới một kiểu duy nhất

Tại sao kiểu PostTag (và tương tự với PostCategory) lại được gọi như vậy, thay vì chỉ là Tag?

Vì, khi thực thi query này (trong đó một sản phẩm là một CPT), kết quả từ trường tags cho posts và sản phẩm sẽ luôn khác nhau, không chồng chéo:

query {
  posts {
    tags {
      id
      name
    }
  }
  products {
    tags {
      id
      name
    }
  }
}

Tags được thêm vào posts sẽ không xuất hiện khi truy xuất tags cho sản phẩm, và ngược lại (trừ khi một sản phẩm cũng sử dụng taxonomy post_tag, nhưng khi đó nó cũng có thể được biểu diễn với kiểu PostTag). Điều này không phải là vấn đề lớn trong WordPress, vì các mục này có thể được coi là các hàng khác nhau từ cùng một bảng cơ sở dữ liệu. Nhưng nó quan trọng đối với GraphQL, vốn được định kiểu mạnh.

Do đó, đây là một quyết định thiết kế tốt để giữ các thực thể này riêng biệt, dưới các kiểu riêng của chúng, và để tags cho posts được trả về dưới kiểu PostTag và, nếu một plugin tùy chỉnh triển khai CPT sản phẩm riêng của mình, nó phải sử dụng kiểu ProductTag cho các tags của mình.

Cấp cho các phần tử media bản sắc riêng của chúng

Các thực thể media trong WordPress là custom post types, chỉ vì điều đó tiện lợi từ quan điểm triển khai. Tuy nhiên, schema GraphQL có thể tránh nợ kỹ thuật này, và mô hình hóa các phần tử media như một thực thể riêng biệt, không phải là custom posts.

Điều này kéo theo các quyết định sau cho schema GraphQL:

  • Kiểu Media không triển khai interface CustomPost, và sẽ không là một phần của kiểu CustomPostUnion
  • Kiểu Media không có nhiều trường được dự kiến từ một custom post type, chẳng hạn như excerpt, datestatus. Thay vào đó, nó chỉ có những trường được dự kiến từ một phần tử media:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Xác định và ánh xạ các enums

Trong một số tình huống, WordPress sử dụng các giá trị cố định từ một tập hợp nhất định. Ví dụ, trạng thái của một post chỉ có thể là "publish", "draft", "pending" hoặc "trash".

Trong GraphQL, chúng ta có thể xử lý chúng như enums (thay vì strings), và tạo một kiểu liệt kê tương ứng. Theo chuẩn GraphQL, enums nên được viết bằng chữ hoa, như thế này:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Tuy nhiên, khi đó query không thể được sử dụng trực tiếp để tương tác với WordPress, vì việc thực thi get_posts( [ "post_status" => "PUBLISH" ] ) không hoạt động.

Vì vậy, như một sự thỏa hiệp, chúng ta giữ các giá trị enum này ở chữ thường:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Ánh xạ các kiểu bổ sung

Blocks không trực tiếp hiển thị trong sơ đồ cơ sở dữ liệu WordPress, vì chúng được lưu trữ trong wp_posts (không có bảng wp_blocks), nhưng dù vậy chúng là một thực thể riêng biệt.

Do đó, chúng ta vẫn có thể giới thiệu một kiểu Block để ánh xạ chúng:

type Post {
  blocks: [Block]
}
 
type Block {
  type: String!
  attributes: JSONObject
}