Chapter 03

Query:精确查询数据

掌握 GraphQL 查询语言的全部特性:字段选择、片段复用、变量传递、指令控制与内省机制

查询语言核心概念

字段选择集(Selection Set)
查询中 {} 包裹的字段列表。GraphQL 只返回客户端声明的字段,多余字段不传输。字段选择集可以嵌套(对象类型内再嵌套字段),构成树状查询结构。
操作(Operation)
一个完整的 GraphQL 请求单元,包括操作类型(query/mutation/subscription)、可选的操作名称和变量声明。一个请求文档可以包含多个操作,但每次执行只运行一个。
片段(Fragment)
可复用的字段选择集,用 fragment 关键字定义,用 ... 展开语法引用。解决多处查询同一字段集的冗余问题,也是 graphql-codegen 代码生成的基本单元。
变量(Variable)
在查询中用 $name: Type 声明,在 variables JSON 对象中传值。使查询文档保持静态(可缓存),参数动态变化。这是 GraphQL 防注入攻击的内置机制。
指令(Directive)
以 @ 开头的修饰符,可附加到字段、片段、变量等,改变查询的执行行为。GraphQL 规范内置 @include、@skip、@deprecated;服务端可以自定义指令(如 @auth、@cacheControl)。
内省(Introspection)
查询 Schema 自身结构的机制,通过 __schema、__type 等元字段实现。GraphQL Playground、Apollo Sandbox 的自动补全功能都依赖内省。生产环境通常应禁用。

基本查询语法

字段选择与嵌套

# 最简单的查询:匿名查询(开发阶段方便,生产不推荐)
{
  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
    }
  }
}
Relay 和组件级片段

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)
) { ... }
变量 vs 硬编码参数的安全考量

直接在查询中写参数(如 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 请求文档结构 操作类型 操作名称 变量列表 ↓ ↓ ↓ query GetUserPosts ($userId: ID!, $limit: Int = 10) { user(id: $userId) { ← 根字段 + 参数 ...UserFields ← 片段展开 posts(limit: $limit) { ← 嵌套字段 + 参数 id title @skip(if: $compact) ← 内联指令 author { ...UserFields ← 同一片段复用 } } } } fragment UserFields on User { ← 片段定义 id name avatar }

本章小结

本章核心要点