查询语言核心概念
基本查询语法
字段选择与嵌套
# 最简单的查询:匿名查询(开发阶段方便,生产不推荐)
{
users {
id
name
email
}
}
# 嵌套对象查询:traversal 图数据结构
{
user(id: "u1") {
name
email
posts { # User.posts 返回 Post 数组
title
publishedAt
author { # Post.author 返回 User(可无限嵌套)
name
avatar
}
comments(first: 3) {
text
likes
}
}
followers {
id
name
}
}
}
无限嵌套的查询可能导致服务器性能问题,甚至 OOM 崩溃(如 user.friends.friends.friends...)。生产环境应设置最大查询深度限制:
// Apollo Server 中限制查询深度
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
validationRules: [depthLimit(7)], // 最多 7 层嵌套
});
另外还有查询复杂度(Query Complexity)限制,防止单次查询消耗过多资源。
参数(Arguments)
# 字段参数:用于过滤、排序、分页等
{
# 标量参数
user(id: "u123") { name }
# 多个参数
users(limit: 10, offset: 0, orderBy: CREATED_AT_DESC) {
id
name
}
# 嵌套字段也可以有参数
post(id: "p1") {
title
comments(first: 5, orderBy: LIKES_DESC) {
text
author { name }
}
# 文件字段带格式参数
coverImage(width: 800, format: WEBP) {
url
}
}
}
别名(Aliases)
别名允许在同一查询中多次调用同一字段(传入不同参数),并给结果中的键重命名。这是 GraphQL 精确控制响应结构的能力:
# 场景1:同一字段不同参数 → 两个结果键
{
publishedPosts: posts(status: PUBLISHED) {
id
title
publishedAt
}
draftPosts: posts(status: DRAFT) {
id
title
updatedAt
}
}
# 响应: { publishedPosts: [...], draftPosts: [...] }
# 场景2:重命名字段以匹配前端组件 Props
{
me: user(id: "current") {
displayName: name # 组件需要 displayName,服务器字段是 name
avatarUrl: avatar # 组件需要 avatarUrl,服务器字段是 avatar
totalPosts: postCount # 语义化重命名
}
}
片段(Fragments)
片段是 GraphQL 查询复用的核心机制,在复杂应用中(如 Relay 驱动的 React 应用),每个组件声明自己需要的片段,父组件组合它们,最终合并为一次查询。
# 定义具名片段:fragment 名称 on 类型
fragment UserBasicFields on User {
id
name
email
avatar
createdAt
}
fragment PostSummary on Post {
id
title
excerpt
publishedAt
author {
...UserBasicFields # 片段可以引用其他片段(组合)
}
}
# 在查询中使用片段
query HomePage {
featuredPosts {
...PostSummary # 展开语法
coverImage { url }
}
currentUser: me {
...UserBasicFields
role
notifications(unread: true) { count }
}
}
# 内联片段:用于联合类型和接口,不需要命名
query SearchResults($term: String!) {
search(term: $term) {
__typename # 内置元字段,返回运行时类型名
... on Post {
title
author { name }
}
... on User {
name
bio
}
... on Product {
name
price
inStock
}
}
}
Facebook 的 Relay 框架把片段的理念发挥到极致:每个 React 组件声明自己所需字段的片段,框架自动将组件树的所有片段合并为一个高效查询。这种"数据获取与组件共置(colocate)"的模式是 GraphQL 最被推崇的实践之一,也是 graphql-codegen 等工具的设计基础。
操作名称与变量
为什么要给查询命名并使用变量?
匿名查询(省略操作类型和名称)在开发调试时方便,但在生产环境有明显缺点:无法在日志中识别具体查询、无法使用持久化查询缓存(APQ)。命名操作 + 变量是生产环境的最佳实践。
# 完整格式:操作类型 + 操作名 + 变量声明
query GetUserWithPosts(
$userId: ID!, # 必填变量,! 表示不能为 null
$postLimit: Int = 5, # 有默认值的可选变量
$includeDrafts: Boolean = false
) {
user(id: $userId) {
name
email
# 在字段参数中使用变量
posts(limit: $postLimit, includeDrafts: $includeDrafts) {
id
title
publishedAt
}
}
}
# 对应的 variables JSON(在请求 body 中传递):
# {
# "userId": "u123",
# "postLimit": 10
# // includeDrafts 不传则使用默认值 false
# }
变量类型声明规则
query TypeExamples(
$id: ID!, # 必须提供,不能为 null
$name: String, # 可以不提供(undefined)或提供 null
$role: UserRole, # 枚举类型变量
$filter: UserFilter!, # input 类型变量(整个对象)
$ids: [ID!]! # 非空 ID 数组(数组本身不能为 null,元素也不能为 null)
) { ... }
直接在查询中写参数(如 user(id: "u123"))容易导致 GraphQL 注入风险(类似 SQL 注入)。变量通过独立的 variables 字段传递,GraphQL 引擎内部处理类型转换,天然防注入。此外,查询字符串固定后可以做持久化查询(Automatic Persisted Queries),大幅降低带宽消耗。
指令(Directives)
内置查询指令
query ConditionalFields(
$showEmail: Boolean!,
$skipBio: Boolean!
) {
user(id: "u1") {
name
# @include(if: Boolean):值为 true 时包含此字段
email @include(if: $showEmail)
# @skip(if: Boolean):值为 true 时跳过此字段(与 @include 语义相反)
bio @skip(if: $skipBio)
# 指令也可以用在片段上
...AdminFields @include(if: $isAdmin)
}
}
fragment AdminFields on User {
role
permissions
lastLoginIp
}
Schema 中的指令
# @deprecated:标记字段废弃,IDE 会显示删除线提示
type User {
name: String!
username: String @deprecated(reason: "Use 'name' instead. Will be removed in v3.0")
profilePic: String @deprecated(reason: "Use 'avatar' field")
avatar: URL
}
# @specifiedBy:为自定义标量指定规范文档 URL
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
scalar Email @specifiedBy(url: "https://html.spec.whatwg.org/#valid-e-mail-address")
# 自定义服务端指令(需要在服务器实现)
type Query {
secretData: String @auth(requires: ADMIN) # 权限控制
cachedUser(id: ID!): User @cacheControl(maxAge: 300) # 缓存控制
}
内省查询(Introspection)
GraphQL 内省(Introspection)是让 Schema 自描述的机制:客户端可以向服务器查询 API 自身的结构。这是 GraphQL 最强大的特性之一——API 文档不需要单独维护,它就在 Schema 里。
# 查询 Schema 中所有类型的概览
{
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
name
kind # OBJECT, SCALAR, ENUM, INPUT_OBJECT, INTERFACE, UNION, LIST
description
}
}
}
# 查询特定类型的字段详情
{
__type(name: "User") {
name
kind
description
fields {
name
description
isDeprecated
deprecationReason
type {
name
kind
ofType { # 用于解析 Non-null 和 List 的包装类型
name
kind
ofType { name kind }
}
}
args {
name
type { name kind }
defaultValue
}
}
}
}
# 查询所有 Mutation 的签名(类似 API 文档)
{
__schema {
mutationType {
fields {
name
description
args {
name
type { name kind ofType { name kind } }
defaultValue
}
type { name kind }
}
}
}
}
内省查询会暴露 API 的完整 Schema 结构(所有类型、字段、参数名称),可能帮助攻击者分析系统漏洞或设计定向攻击。生产环境标准做法:
// Apollo Server 4:根据环境控制
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production',
});
如果需要对合作伙伴提供 Schema 文档,推荐使用 Apollo Schema Registry 或 GraphQL Inspector 等工具,以受控方式分享 Schema,而不是开放内省端点。
查询文档结构总结
本章小结
- 字段选择是 GraphQL 的灵魂:客户端精确声明需要哪些字段(不多不少),服务器按声明返回,彻底解决了 REST 的过度/欠获取问题;嵌套字段选择可以在一次请求中获取关联数据图。
- 片段消除重复:fragment 定义可复用的字段集,在多处用 ... 展开;内联片段(... on Type)用于处理联合类型和接口,配合 __typename 字段做运行时类型判断。
- 变量是生产环境必须:$name: Type 声明变量,在 variables 对象中传值;查询文档保持静态(可缓存/持久化),参数动态变化;同时天然防止注入攻击。
- @include/@skip 条件字段:根据变量值动态包含或排除字段,避免为不同场景写多个查询;这在移动端(不同屏幕尺寸需要不同字段)和权限控制(管理员字段)场景特别有用。
- 内省机制支撑工具生态:GraphQL Playground/Apollo Sandbox 的自动补全、graphql-codegen 的类型生成、GraphQL Voyager 的 Schema 可视化都依赖内省;生产环境应禁用(
introspection: false)防止 Schema 信息泄露。