GraphQL 模式 无需样板代码即可编写、格式化和理解 SDL
GraphQL 模式是服务器和客户端之间的自文档化协议。本指南将从零开始介绍模式定义语言(SDL),涵盖类型、标量、查询、变更、输入类型、枚举和接口,使您能够自信地阅读和编写模式。
如果你曾使用过REST API,然后第一次接触GraphQL模式,语法可能看起来熟悉但无法解析。这是正常的。模式定义语言(SDL)简洁,但每行都承载着大量含义。本文将详细解释,让你无需每五分钟就查阅文档,就能阅读和编写模式。
GraphQL模式实际上是什么
GraphQL模式是你的API服务器与每个调用它的客户端之间的契约。它精确地定义了数据的存在、可用的操作以及响应的结构。与REST不同——在REST中,你通过文档、试错来发现端点——GraphQL让契约明确且可被机器读取。
每个GraphQL API都从一个用SDL编写的模式开始。服务器在运行时会验证所有查询与该模式的一致性。如果客户端请求了一个在模式中不存在的字段,请求将在任何解析器运行之前被拒绝。这是GraphQL类型系统的核心价值:模式是API表面范围的唯一真实来源。
SDL基础:类型、查询和变更
每个SDL文件都是由类型定义构建而成。以下是最小的可用模式示例:
type Query {
hello: String
}
Query 是读取操作的入口点。每个模式都必须包含一个。字段 hello 返回一个 String ——五种内置标量类型之一。
五种内置标量类型是:
- 细绳 ——UTF-8文本
- Int ——32位有符号整数
- Float ——双精度浮点数
- Boolean ——真或假
- ID ——一个唯一标识符,以字符串形式序列化
实际的模式会定义自定义对象类型。以下是一个更典型的模式示例:
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
body: String
author: User!
publishedAt: DateTime
}
type Query {
user(id: ID!): User
posts: [Post!]!
}
这20行中发生了很多事情。让我们逐一解析。
非空字段和列表
这 ! 在类型后表示该字段是非空的——它永远不会返回 null。如果没有这个修饰,该字段是可空的,客户端必须处理缺失值。在运行时,这种区别至关重要:如果一个非空字段的解析器抛出异常或返回null,GraphQL会将null向上传播,可能使父级字段也变为null。
列表使用方括号: [Post] 是一个可空的、包含可空帖子的列表。 [Post!]! 是一个非空的、包含非空帖子的列表。这是你通常想要的版本——一个始终返回数组(可能为空)的字段,且数组中的每个元素始终是真实对象,而不是null。
四种组合的详细说明:
[Post]——可空列表,可空项(很少有用)[Post!]——可空列表,非空项[Post]!——非空列表,可空项[Post!]!——非空列表,非空项(最常见)
变更:写入数据
查询是只读的。变更处理写入操作。类型 Mutation 的结构与 Query相同,但根据约定,其字段具有副作用:
type Mutation {
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
注意 CreatePostInput! ——这是一个输入类型,这直接引出了下一个概念。
输入类型与输出类型
这是GraphQL模式中最常见的混淆点之一。你 不能 将一个像 Post 这样的对象类型重新用于变更的参数。SDL之所以有这两种类型,是有原因的:
- 对象类型 (
type) ——用于响应。可以包含解析器和复杂字段逻辑。 - 输入类型 (
input) ——用于参数。纯数据,无解析器。字段只能引用标量或其它输入类型。
input CreatePostInput {
title: String!
body: String
authorId: ID!
}
这种分离保持了参数验证的清晰性,并防止了如果输出类型(可以在复杂图中相互引用)直接用作输入时可能出现的循环引用问题。
自定义标量
五种内置标量类型并不能涵盖所有情况。日期时间字符串、URL、JSON块等需要自定义标量。你可以这样声明它们:
scalar DateTime
scalar JSON
scalar URL
标量声明告诉SDL使用哪个名称。实际的序列化、反序列化和验证逻辑存在于服务器实现中——而不是在模式文件中。像 graphql-scalars 这样的库提供了常见类型的现成实现,因此你无需从头编写。
当一个 String 在技术上是正确的,但在语义上是错误的时,使用自定义标量。一个字段类型为 DateTime 传达了意图;而一个字段类型为 String 迫使客户端猜测格式。
枚举
枚举将字段限制在一组固定的字符串值内。相比于原始字符串,枚举更可靠,因为模式在类型级别强制规定了允许的值:
enum UserRole {
ADMIN
EDITOR
VIEWER
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
一个字段类型为 UserRole 永远不会返回意外的字符串。一个字段类型为 String 可能返回任何值。当值集合已知、稳定且对客户端有意义时,请使用枚举——这使得模式探测显著更有用。
接口和联合
当一个字段可以返回不同类型对象时,GraphQL提供了两种机制:接口和联合。
接口 定义一组共享的字段。实现接口的类型必须包含这些字段:
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Post implements Node {
id: ID!
title: String!
}
联合 将类型分组,而无需要求共享字段。当可能的类型在结构上没有共同点时,联合非常有用:
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
客户端使用内联片段来处理联合或接口中的每种类型: ... on User { name }。如果你返回的是多态数据,当类型共享字段时,优先使用接口;当它们不共享字段时,使用联合。
指令
指令在字段或类型级别修改行为。每个GraphQL实现都内置了两个指令:
@deprecated(reason: "Use newField instead")——在探测中标记一个字段为已弃用@skip(if: Boolean)且@include(if: Boolean)——在查询中作为客户端条件使用
你也可以为诸如身份验证守卫、速率限制提示或缓存注解等场景定义自定义指令。它们在模式中声明,并在服务器上实现:
directive @auth(requires: UserRole = ADMIN) on FIELD_DEFINITION
type Mutation {
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
}
GraphQL与REST:何时使用哪种
GraphQL的模式优先方法在特定情况下表现优异。当满足以下条件时,它是正确的选择:
- 多个客户端(网页、移动、第三方)需要相同数据的不同子集
- 你希望在不进行版本控制的情况下演进API——可以自由添加字段,弃用而非删除
- 领域天然具有复杂实体之间的关系结构
- 你希望拥有一个自我文档化的API,客户端可以通过探测来探索它
当满足以下条件时,REST仍然是更好的选择:
- 你正在构建具有可预测、扁平负载的简单CRUD端点
- HTTP缓存至关重要——GraphQL的POST请求默认不支持缓存
- 团队规模小且API表面稳定——REST的开销更小
- 你遇到了N+1问题,并且不想配置DataLoader来解决它
N+1问题值得暂停考虑。在一个GraphQL解析器中,当它获取 posts 和每个帖子的 author时,一个朴素的实现会为每个帖子发起一次数据库查询来加载作者。对于50个帖子,这将导致一个请求51次查询。REST没有这个问题,因为你可以精确控制每个端点运行的SQL语句。在GraphQL中,你通过批处理(DataLoader或等效方案)来解决它——这是可以解决的,但需要正确配置。
格式化和验证你的SDL
随着团队的增长,模式文件会逐渐积累不一致的缩进、间距不匹配和结构漂移。在提交模式变更前,请运行格式化工具以标准化间距、排序字段,并在进入CI前发现语法错误。
这 GraphQL Schema Formatter on iotools.cloud 正是这样做的——粘贴你的SDL,获得格式整洁、一致的输出,并在行内显示验证错误。它支持多类型模式、自定义标量、指令以及手动格式化容易出错的边缘情况。作为提交或与另一团队共享模式前的最终检查非常有用。
