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 的字段可以并行解析,这是两者在语义上的重要差别。

八、查询与变更的语法结构对照

查询与变更共享同一套选择集语法,差别在于根类型与执行语义。下表对照两者的关键结构。

结构要素querymutation
根类型QueryMutation
执行顺序字段可并行字段串行
典型用途读取、筛选、分页创建、更新、删除
返回形状选择集同构选择集同构,常含结果对象
变量与指令均可用均可用

下面这张图把一次完整请求的语法结构串起来:从操作声明、变量定义,到字段、参数、别名、片段展开与指令,最终映射为服务端解析与 JSON 返回。

GraphQL 请求文档的语法结构与执行映射 操作声明 Operation query BookDetail($id: ID!) mutation BorrowBook($id: ID!) 操作类型 + 操作名称 + 变量定义 变量 Variables $id: ID! $withPrice: Boolean! 独立 JSON 传入,文档保持静态 {"id":"B-1001","withPrice":true} 选择集 Selection Set book(id: $id) { title price @include(if: $withPrice) ...BookCore cheap: books(maxPrice: 50) } 字段 · 参数 · 别名 · 片段 · 指令 嵌套对象继续展开选择集 标量字段不可再展开 片段 Fragment fragment BookCore on Book { id title author { name } } 通过 ...BookCore 展开复用 服务端解析 Resolver 校验字段与参数类型 读取 args 与 variables 按选择集裁剪数据 query 并行 / mutation 串行 JSON 返回 data.book.title data.book.price data.cheap[0].title 结构与请求同形,键名受别名影响

九、一个可执行的完整示例

把上述要素组合起来,下面是一份可直接提交的文档与变量。它同时演示了操作名称、变量、别名、片段与指令。

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 控制可选字段,避免为不同展示场景维护多份文档。
常见错误:把对象字段当作标量直接请求(缺少选择集)会触发语法错误;变量声明了却未使用、或使用了未声明的变量,都会在校验阶段被拒绝;mutation 中依赖前一个字段结果的场景必须放在同一 mutation 内按顺序书写,不能拆成多个并行请求。

掌握这些语法要素后,客户端可以用一份静态文档配合不同变量覆盖大量读取与写入场景,服务端也能据此稳定地校验、解析并裁剪数据。下一阶段将进入 schema 定义与类型系统,理解这些语法背后的类型约束从何而来。