3. GraphQL查询语言精要
GraphQL 的查询语言(Query Language)是一套面向文档的声明式语法。客户端提交的文档(Document)由若干操作(Operation)组成,每个操作声明自己要读取或修改的数据形状,服务端按字段解析并返回同构的 JSON。理解这套语法,是写出可维护、可复用、可安全传参的 GraphQL 请求的前提。本节围绕一个图书借阅服务的示例 schema 展开,逐层拆解字段、参数、别名、片段、变量、指令、操作名称,以及查询与变更的语法结构。
一、字段:选择集与嵌套形状
字段(Field)是查询的最小选择单位。最外层字段对应 Query 类型上的入口,花括号内的字段构成该对象的“选择集”(Selection Set)。GraphQL 的返回结构与请求结构同形,这是它区别于 REST 的核心特征。
query {
book(id: "B-1001") {
title
author { name }
price
}
}
上面请求返回的 JSON 顶层是 data.book,其下恰好是 title、author.name、price 三个键。标量字段(String、Int、Float、Boolean、ID、枚举)不能再展开选择集;对象字段必须给出选择集,否则语法报错。字段名区分大小写,且必须与 schema 中定义一致,拼写错误会在校验阶段被拒绝,而不是运行期静默返回 null。
二、参数:字段级输入
参数(Argument)写在字段名后的圆括号中,用于筛选、分页或指定标识。参数可以出现在任意字段上,不限于根字段。参数值可以是标量字面量、枚举、列表或输入对象。
query {
books(category: "TECH", first: 2, orderBy: PRICE_ASC) {
title
price
}
}
参数类型由 schema 约束:first 是 Int,orderBy 是枚举。若传入字符串 "2" 给 Int 参数,校验会失败。参数是 GraphQL 实现“按需取数”的关键,服务端解析器(Resolver)通过 args 读取它们,再决定数据源查询条件。
三、别名:同一字段多次请求
当同一字段需要以不同参数请求多次时,返回键会冲突。别名(Alias)用 别名: 字段名 的语法重命名结果键,使一个请求内可以并列多个同名字段。
query {
cheap: books(maxPrice: 50) { title price }
expensive: books(minPrice: 100) { title price }
}
返回的 data 中会出现 cheap 与 expensive 两个键,而不是两个 books。别名也常用于在字段重命名或迁移期间保持客户端兼容。
四、片段:可复用的选择集
片段(Fragment)把一组字段抽取为命名单元,通过 ... 展开。它减少重复,并让同一形状在多个操作间共享。片段必须声明其适用的类型(on TypeName)。
fragment BookCore on Book {
id
title
author { name }
}
query {
book(id: "B-1001") {
...BookCore
price
}
}
片段可以嵌套片段,也可以配合接口或联合类型使用内联片段(... on EBook { fileSize })按具体类型取字段。片段是客户端工程化的基础,通常与代码生成工具配合,把片段映射为强类型模型。
五、变量:把动态值移出文档
变量(Variable)让查询文档保持静态,动态值通过独立的 variables 对象传入。变量在操作名后以 $name: Type 声明,在字段参数中以 $name 引用。
query BookDetail($id: ID!, $withPrice: Boolean!) {
book(id: $id) {
title
price @include(if: $withPrice)
}
}
变量类型后的 ! 表示非空。变量是安全传参的正道:它避免了字符串拼接注入,也让服务端可以缓存查询文档、只变化参数。实际调用时,variables 是一个 JSON 对象,例如 {"id": "B-1001", "withPrice": true}。
六、指令:条件化地包含或跳过字段
指令(Directive)以 @ 开头,附着在字段、片段展开或操作上,改变执行行为。内置指令有两个:@include(if: Boolean) 与 @skip(if: Boolean)。前者在条件为 true 时包含该字段,后者在条件为 true 时跳过该字段。
query {
book(id: "B-1001") {
title
price @include(if: $withPrice)
stock @skip(if: $hideStock)
}
}
指令让客户端用一份文档覆盖多种展示场景,减少请求分支。服务端也可以定义自定义指令,但需在 schema 中声明并实现相应逻辑。
七、操作名称与操作类型
一个文档可以包含多个操作。操作名称(Operation Name)写在操作类型之后,用于在多个操作中指定执行哪一个,也便于日志与性能追踪。操作类型有三种:query(读取)、mutation(写入)、subscription(订阅)。
query GetBook { book(id: "B-1001") { title } }
mutation BorrowBook($id: ID!) {
borrowBook(id: $id) { ok dueDate }
}
当文档只有一个操作时,操作名称和 query 关键字都可省略;但生产环境建议始终显式命名,便于排查。mutation 的字段按顺序串行执行,而 query 的字段可以并行解析,这是两者在语义上的重要差别。
八、查询与变更的语法结构对照
查询与变更共享同一套选择集语法,差别在于根类型与执行语义。下表对照两者的关键结构。
| 结构要素 | query | mutation |
|---|---|---|
| 根类型 | Query | Mutation |
| 执行顺序 | 字段可并行 | 字段串行 |
| 典型用途 | 读取、筛选、分页 | 创建、更新、删除 |
| 返回形状 | 选择集同构 | 选择集同构,常含结果对象 |
| 变量与指令 | 均可用 | 均可用 |
下面这张图把一次完整请求的语法结构串起来:从操作声明、变量定义,到字段、参数、别名、片段展开与指令,最终映射为服务端解析与 JSON 返回。
九、一个可执行的完整示例
把上述要素组合起来,下面是一份可直接提交的文档与变量。它同时演示了操作名称、变量、别名、片段与指令。
query BookDetail($id: ID!, $withPrice: Boolean!) {
book(id: $id) {
...BookCore
price @include(if: $withPrice)
}
cheap: books(maxPrice: 50) { title price }
}
fragment BookCore on Book {
id
title
author { name }
}
{
"id": "B-1001",
"withPrice": true
}
提交后,若 id 存在,返回的 data.book 含 id、title、author.name 与 price;data.cheap 是价格不高于 50 的图书数组。若把 withPrice 改为 false,price 键会从返回中消失,而不是返回 null。若 id 不存在,book 为 null,同时 errors 数组会给出定位信息。判断请求是否成功,应同时检查 errors 是否存在以及 data 中目标键是否符合预期,而不是只看 HTTP 状态码。
@include/@skip 控制可选字段,避免为不同展示场景维护多份文档。掌握这些语法要素后,客户端可以用一份静态文档配合不同变量覆盖大量读取与写入场景,服务端也能据此稳定地校验、解析并裁剪数据。下一阶段将进入 schema 定义与类型系统,理解这些语法背后的类型约束从何而来。