插件如何将WordPress数据模型映射到GraphQL模式
这是 Gato GraphQL 将 WordPress 数据模型映射到对应 GraphQL 模式的方式。
WordPress 数据模型
WordPress 包含以下实体:
- posts
- pages
- custom posts
- 媒体元素
- 用户
- 用户角色
- 标签
- 分类
- 评论
- 块
- 元属性
- 其他(选项、插件、主题等)
这些实体可以具有层级关系。例如,post、page 和媒体元素都是自定义文章类型,而标签和分类都是分类法。
以下是 WordPress 数据库图,展示了所有实体的数据存储方式:

映射是数据库图的精确副本吗?
在将 WordPress 数据库映射为 GraphQL 模式时,上述图是否会被一比一地还原?
不,并非如此。数据库图是实际的实现,而 GraphQL 是从客户端访问数据的接口。两者相关,但可以有所不同。GraphQL 不关心数据库:它不以 SQL 命令思考,也不知道存在名为 wp_posts 和 wp_users 的数据库表。
因此,在为 WordPress 创建 GraphQL 模式时,无需过多关注数据库图。更进一步,我们还可以生成一个修复 WordPress 数据模型部分技术债务的 GraphQL 模式。
将WordPress数据模型映射为GraphQL模式
让我们来进行映射。首先,尽可能将原始实体映射为类型。从 WordPress 数据模型的实体列表中,为 GraphQL 模式生成以下类型:
PostPageMediaUserUserRolePostTagPostCategoryComment
然后,为每个类型添加所有预期字段。为了表示模式,可以使用 SDL(模式定义语言)。(这仅用于文档目的;插件本身不使用 SDL 来编码模式:全部使用 PHP 代码。)
以下是 Post 的字段(众多字段中的一部分):
type Post {
id: ID!
title: String
content: String
excerpt: String
date: Date!
}以下是 User 的字段(众多字段中的一部分):
type User {
id: ID!
name: String
email: String!
}我们还创建对应的连接,即返回另一个实体(而非标量,如数字或字符串)的字段。例如,表示文章有作者,以及用户拥有文章:
type Post {
author: User!
}
type User {
posts: [Post]
}字段和连接也可以接受参数。例如,使 Post.dateStr 可被格式化,以及使 User.posts 能够过滤条目、限制数量并进行排序:
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
}
# ...我们对 WordPress 数据模型中的所有实体继续执行此操作。完成后,将得到 WordPress 的 GraphQL 模式,可通过 Voyager 客户端(在插件菜单中作为"Interactive Schema"提供)查看:

该模式与 WordPress 数据库图有相似之处,但也存在一些差异。让我们来分析它们。
无实体的操作映射为Root字段
WordPress 数据库图表示数据的存储方式,因此没有"起点"。而 GraphQL 是用于检索数据的接口,因此必须有一个执行 Query 的初始阶段。
这个初始阶段就是 Root 类型,更准确地说是 QueryRoot 和 MutationRoot 类型(分别用于处理 queries 和 mutations)。
在这两个类型中,我们映射所有不依赖于实体的操作,例如执行 get_posts()、get_users() 或 wp_signon() 时:
type QueryRoot {
posts: [Post]!
users: [User]!
}
type MutationRoot {
loginUser(
usernameOrEmail: String!,
password: String!
): User
}字段不需要与其所代表的操作具有相同的名称或签名。例如,调用字段 loginUser 可能比 signOn 更为合适。
分组模式元素
我们可以应用增强措施来简化模式并使其更易用。例如,字段可以通过一个 input 对象接收所有参数,该对象可在多个字段之间复用,并使模式更易于可视化:
type MutationRoot {
loginUser(input: LoginUserByInput!): User
}
input LoginUserByInput {
usernameOrEmail: String!,
password: String!
}此外,mutation 的响应可以是一个"payload"对象,除了返回受影响的对象外,还可以包含操作状态和错误消息:
type MutationRoot {
loginUser(input: LoginUserByInput!): RootLoginUserMutationPayload!
}
type RootLoginUserMutationPayload {
errors: [RootLoginUserMutationErrorPayloadUnion!]
status: OperationStatusEnum!
user: User
userID: ID
}
union RootLoginUserMutationErrorPayloadUnion = GenericErrorPayload
| InvalidUserEmailErrorPayload
| InvalidUsernameErrorPayload
| PasswordIsIncorrectErrorPayload
| UserIsLoggedInErrorPayload所有mutation都位于MutationRoot下
有些操作依赖于特定实体,例如 wp_update_post(),它作用于某篇文章。GraphQL 模式中对应的 mutation 必须添加到 MutationRoot 类型,因为这是 GraphQL 的工作方式。
该操作的映射如下:
type MutationRoot {
updatePost(input: RootUpdatePostFilterInput!): PostUpdateMutationPayload!
}
input RootUpdatePostFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
id: ID!
status: CustomPostStatusEnum
tags: [String!]
title: String
}该插件还支持嵌套 mutations,作为可选功能提供(因为这不是标准 GraphQL 行为)。这样,mutations 也可以添加到任何类型下,而不仅限于 MutationRoot。在这种情况下,我们得到:
type Post {
update(input: PostUpdateFilterInput!): PostUpdateMutationPayload!
}
input PostUpdateFilterInput {
categoryIDs: [ID!]
content: String
featuredImageID: ID
status: CustomPostStatusEnum
tags: [String!]
title: String
}请注意 RootUpdatePostFilterInput 与 PostUpdateFilterInput 之间的区别(即根级 mutations 与嵌套 mutations 之间的区别):前者有必填属性 id 以指示要修改哪篇文章,而后者则不需要该属性。
处理custom posts
GraphQL 中没有类型继承。因此,我们无法定义 CustomPost 类型,并声明 Post 和 Page 继承自它。
GraphQL 提供了两种资源来弥补这一不足:接口和联合类型。
对于第一种资源,我们在模式中创建一个 CustomPost 接口,声明 custom post 所期望的所有字段,并定义 Post、Page 和 GenericCustomPost(表示已安装的主题和插件定义的所有 custom post 类型)来实现该接口:
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!
}对于第二种资源,我们在模式中创建一个 CustomPostUnion 类型,返回所有 custom post 类型:
union CustomPostUnion = Post | Page | GenericCustomPost并在适当的情况下让字段返回此类型:
type QueryRoot {
customPost(id: ID): CustomPostUnion
customPosts: [CustomPostUnion]!
}
type User {
customPosts: [CustomPostUnion]
}
type Comment {
customPost: CustomPostUnion!
}在执行 Query 时,可以根据实际类型(如 Post)或 CustomPost 接口来选择字段:
{
customPosts {
__typename
...on CustomPost {
id
title
slug
status
}
...on Post {
isSticky
postFormat
}
}
}可以看出,在 GraphQL 模式中需要明确区分处理的是 posts 还是 custom posts,因为两者并不相同!将这两者互换使用是 WordPress 的技术债务,插件会尽可能地修复这一问题。
因此,custom post 始终被称为 CustomPost 而非 Post,处理 custom posts 的字段始终被称为 customPosts 而非 posts,接收 custom post ID 的字段参数被称为 customPostID 而非 postID(即使在映射的 WordPress 函数中就是这么叫的)。
这样,期望始终清晰明确:
- 字段
User.customPosts可以返回任意 custom post 的列表,包括 posts 和 pages,而User.posts只返回 posts - 字段
Root.setFeaturedImageOnCustomPost可以为任意 custom post 添加特色图片,这就是为什么它不叫setFeaturedImageOnPost
不将标签(和分类)归入单一类型
为什么 PostTag 类型(PostCategory 同理)要这样命名,而不是直接叫 Tag?
因为在执行以下 Query(其中 product 是 CPT)时,posts 和 products 的 tags 字段结果将始终不同且不重叠:
query {
posts {
tags {
id
name
}
}
products {
tags {
id
name
}
}
}添加到 posts 的标签在获取 products 的标签时不会显示,反之亦然(除非 product 也使用 post_tag 分类法,但即便如此,也可以用 PostTag 类型表示)。这在 WordPress 中并不是大问题,因为这些条目可以被视为同一数据库表中的不同行。但对于强类型的 GraphQL 来说,这一点很重要。
因此,将这些实体分别保存在各自的类型下是一个良好的设计决策,posts 的标签在 PostTag 类型下返回,如果自定义插件实现了自己的 product CPT,则其标签必须使用 ProductTag 类型。
赋予媒体项独立的身份
WordPress 中的媒体实体是 custom post 类型,仅仅是因为这在实现上比较方便。然而,GraphQL 模式可以避免这一技术债务,将媒体元素建模为独立的实体,而非 custom posts。
这对 GraphQL 模式意味着以下决策:
Media类型不实现CustomPost接口,也不会成为CustomPostUnion类型的一部分Media类型没有 custom post 类型所期望的许多字段,例如excerpt、date和status。相反,它只有媒体元素所期望的字段:
type Media {
id: ID!
src: String!
width: Int
height: Int
}识别和映射枚举
在某些情况下,WordPress 使用来自特定集合的固定值。例如,文章的状态只能是 "publish"、"draft"、"pending" 或 "trash"。
在 GraphQL 中,我们可以将这些值视为枚举(而非字符串),并创建对应的枚举类型。遵循 GraphQL 标准,枚举应使用大写字母,如下所示:
enum CUSTOM_POST_STATUS {
PUBLISH
DRAFT
PENDING
TRASH
}然而,这样一来,Query 就无法直接用于与 WordPress 交互,因为执行 get_posts( [ "post_status" => "PUBLISH" ] ) 不会生效。
因此,作为折中方案,我们保持这些枚举值为小写:
enum CUSTOM_POST_STATUS {
publish
draft
pending
trash
}映射额外类型
块在 WordPress 数据库图中不直接可见,因为它们存储在 wp_posts 中(不存在 wp_blocks 表),但它们仍然是独立的实体。
因此,我们仍可以引入 Block 类型来映射它们:
type Post {
blocks: [Block]
}
type Block {
type: String!
attributes: JSONObject
}