GraphQL 简介

Schema 与类型系统

掌握 GraphQL 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
StringUTF-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
        }
    }
}
类型之间允许互相引用(像上面 User 和 Article 互相指),Schema 层面没有问题。具体每层数据怎么取,由 resolver 决定,下一篇讲查询时会看到完整实现。

💡 枚举与注释

有些字段的取值是固定的几个选项,比如用户角色只有「管理员」和「普通用户」。这种情况用**枚举(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 里,既是校验依据也是接口文档。
  • 内置标量类型有五种:IntFloatStringBooleanID,id 类字段用 ID 语义更准确。
  • ! 表示非空,[] 表示列表,[User!]! 是列表字段的常用写法。
  • 自定义类型可以互相引用来表达关联,客户端一次查询即可嵌套取数。
  • 固定取值用 enum 更严谨,能给类型和字段写 """ 描述当作文档。

❓ 知识问答

Q1:IDString 有什么区别?

底层都是字符串,区别在于语义:ID 明确表示「这个字段是唯一标识,不打算展示给人看」。工具和开发者看到 ID 就知道它是主键,看到 String 则认为是普通文本。

Q2:为什么 Int 不能存毫秒时间戳?

毫秒时间戳(如 1753000000000)超过了 32 位整数的上限(约 21 亿),校验时会直接报错。这类大数值用 String 表示。

Q3:[User!]![User] 在实际返回上有什么差别?

前者保证返回的要么是有内容的数组、要么是空数组,客户端直接使用即可;后者可能返回 null,数组里还可能混着 null,客户端每次都得判空,很容易漏。

Q4:枚举的值能写中文吗?

不建议。枚举值会出现在查询语句里,写中文容易在编码、输入法上出问题,遵循大写英文加下划线的惯例更稳妥,展示文案交给前端处理。

Q5:typeDefs 写错了会怎样?

服务启动时就会报错并指出出错位置,比如类型没定义、括号不匹配等。Schema 语法问题在启动阶段就能暴露,不会留到运行中。


🧪 小练习

练习一:为你自己的「文章」数据设计一个 Article 类型,包含 id、标题、发布时间(想想该用什么类型)、作者,并写出它和 User 的关联。

# 请在这里编写代码

练习二:给用户加上「状态」字段,取值只有「启用」和「禁用」两种,用枚举实现,并把 role 字段设为必有的非空字段。

# 请在这里编写代码

🎉 恭喜你已经掌握 GraphQL 类型系统的常用写法啦!下一篇我们回到查询本身,学习参数、变量和嵌套查询,看看客户端怎么把 Schema 里的能力用出来。