Schema 与类型系统
🎯 引言
上一篇我们提到,GraphQL 服务收到查询后,会先拿它和 Schema 做校验,字段写错了立刻报错。可以说 Schema 是整个 GraphQL 服务的地基:它既是服务端的数据说明书,也是客户端写查询时的依据。
学完这篇文章,你能熟练使用 GraphQL 类型系统的常用写法:五种标量类型、非空和列表标记、枚举,以及如何用自定义类型描述数据之间的关联,写出一个结构清晰、能当文档用的 Schema。
🧱 Schema 是什么
Schema 是用 GraphQL 语言写的一份「数据契约」,它明确告诉客户端:这个服务里有哪些数据、每个字段叫什么名字、是什么类型、能不能为空。
在 Apollo Server 里,Schema 就是 typeDefs 这个字符串:
const typeDefs = `#graphql
type User {
id: ID!
username: String!
age: Int
}
type Query {
users: [User!]!
}
`;
可以把它理解成菜单:菜单上写清楚的菜,顾客才能点;菜单上没有的,点了也会被告知「没有这道菜」。type User 定义了一种数据的结构,type Query 则定义了客户端能从哪些入口开始查询。
因为 Schema 是强类型的、结构固定的,很多工具可以直接读取它生成文档和代码提示,这就是 GraphQL「自带文档」这个说法的来源。
🧱 标量类型
标量类型是类型的最小单位,下面不能再分字段。GraphQL 内置了五种:
| 类型 | 含义 | 示例 |
|---|---|---|
Int | 有符号 32 位整数 | 20 |
Float | 双精度浮点数 | 99.5 |
String | UTF-8 字符串 | "小明" |
Boolean | 布尔值 | true / false |
ID | 唯一标识符 | "1" |
前四种和 JavaScript 里的概念一一对应,不用多解释。ID 需要单独说一句:它本质上也是字符串,但语义上表示「唯一标识」,通常用于主键。写 Schema 时,表示 id 的字段都用 ID 而不是 String,语义更准确。
Int 是 32 位整数,范围大约是正负 21 亿。像手机号、时间戳(毫秒)这类可能超过范围的数值,应该用 String 来表示,避免溢出。✨ 非空与列表
光说「是什么类型」还不够,实际开发中还要表达「能不能为空」「是单个还是一组」。GraphQL 用两个标记来解决:
- 感叹号
!:表示非空,这个字段一定会返回值,不可能是null。 - 方括号
[]:表示列表,字段值是一个数组。
它们可以组合,比如 [User!]! 这种写法看着绕,拆开读就清楚了:
type Query {
users: [User!]!
}
从左到右读:users 是一个列表,列表里的每一项是 User 且不为 null,整个列表本身也不为 null。也就是说最差情况下返回一个空数组 [],而不会返回 null。
几种常见组合的含义对比:
| 写法 | 列表本身能为 null | 列表项能为 null |
|---|---|---|
[User] | 可以 | 可以 |
[User]! | 不可以 | 可以 |
[User!] | 可以 | 不可以 |
[User!]! | 不可以 | 不可以 |
日常开发中 [User!]! 是列表字段的常用写法,语义最严格,客户端处理起来也省心,不用判空。
🛠 自定义类型与关联
实际项目的数据往往不是孤立的:用户会发文章,文章属于某个用户。GraphQL 用自定义类型互相引用来表达这种关联:
type Article {
id: ID!
title: String!
author: User!
}
type User {
id: ID!
username: String!
articles: [Article!]!
}
这里有两个要点:
Article.author的类型是另一个自定义类型User,表示「这篇文章的作者是一个用户」。User.articles的类型是[Article!]!,表示「这个用户有一组文章」。
关联写好之后,客户端就能在一次查询里嵌套取数,想取几层取几层:
{
users {
username
articles {
title
}
}
}
💡 枚举与注释
有些字段的取值是固定的几个选项,比如用户角色只有「管理员」和「普通用户」。这种情况用**枚举(Enum)**比 String 更严谨:
enum Role {
ADMIN
USER
}
type User {
id: ID!
username: String!
role: Role!
}
枚举的值习惯用大写下划线命名。客户端传入或查询时,如果值不在这两个选项里,会直接校验失败,从源头挡住了脏数据。
另外,Schema 支持用 """ 给类型和字段写描述,它会显示在 Apollo Sandbox 的文档面板里:
"""系统用户"""
type User {
"""用户唯一标识"""
id: ID!
"""昵称,允许重复"""
username: String!
}
花几秒钟给关键字段写上描述,后面接手的同事(和几个月后的自己)都会轻松不少。
🧾 小节总结
- Schema 是服务端的数据契约,写在
typeDefs里,既是校验依据也是接口文档。 - 内置标量类型有五种:
Int、Float、String、Boolean、ID,id 类字段用ID语义更准确。 !表示非空,[]表示列表,[User!]!是列表字段的常用写法。- 自定义类型可以互相引用来表达关联,客户端一次查询即可嵌套取数。
- 固定取值用
enum更严谨,能给类型和字段写"""描述当作文档。
❓ 知识问答
Q1:ID 和 String 有什么区别?
底层都是字符串,区别在于语义:ID 明确表示「这个字段是唯一标识,不打算展示给人看」。工具和开发者看到 ID 就知道它是主键,看到 String 则认为是普通文本。
Q2:为什么 Int 不能存毫秒时间戳?
毫秒时间戳(如 1753000000000)超过了 32 位整数的上限(约 21 亿),校验时会直接报错。这类大数值用 String 表示。
Q3:[User!]! 和 [User] 在实际返回上有什么差别?
前者保证返回的要么是有内容的数组、要么是空数组,客户端直接使用即可;后者可能返回 null,数组里还可能混着 null,客户端每次都得判空,很容易漏。
Q4:枚举的值能写中文吗?
不建议。枚举值会出现在查询语句里,写中文容易在编码、输入法上出问题,遵循大写英文加下划线的惯例更稳妥,展示文案交给前端处理。
Q5:typeDefs 写错了会怎样?
服务启动时就会报错并指出出错位置,比如类型没定义、括号不匹配等。Schema 语法问题在启动阶段就能暴露,不会留到运行中。
🧪 小练习
练习一:为你自己的「文章」数据设计一个 Article 类型,包含 id、标题、发布时间(想想该用什么类型)、作者,并写出它和 User 的关联。
# 请在这里编写代码
练习二:给用户加上「状态」字段,取值只有「启用」和「禁用」两种,用枚举实现,并把 role 字段设为必有的非空字段。
# 请在这里编写代码
🎉 恭喜你已经掌握 GraphQL 类型系统的常用写法啦!下一篇我们回到查询本身,学习参数、变量和嵌套查询,看看客户端怎么把 Schema 里的能力用出来。
