Graph QL Schema

GraphQL-Schema

#resource / graphql #type / concept #status / growing

GraphQL-Schema

基础概念

什么是 GraphQL Schema

GraphQL Schema 是 GraphQL API 的核心,它定义了 API 的结构、可用的操作、数据类型以及各字段之间的关系。Schema 充当客户端和服务器之间的契约,明确规定了客户端可以请求哪些数据以及如何请求。

Schema 的作用

  • 类型定义:定义数据的结构和关系
  • API 契约:明确客户端可以执行的操作
  • 文档化:自动生成 API 文档
  • 验证:确保请求的合法性
  • 工具支持:为开发工具提供类型信息

Schema 核心组件

  1. 类型系统(Type System)
  2. 根类型(Root Types)
  3. 字段解析器(Resolvers)
  4. 指令(Directives)
  5. 模式定义语言(Schema Definition Language, SDL)

使用指南

基本类型系统

标量类型(Scalar Types):

# 内置标量类型
scalar Int      # 32位整数
scalar Float    # 双精度浮点数
scalar String   # UTF-8字符串
scalar Boolean  # true/false
scalar ID       # 唯一标识符

# 自定义标量类型
scalar DateTime
scalar Email
scalar URL
scalar JSON

对象类型(Object Types):

type User {
  id: ID!
  username: String!
  email: Email!
  profile: Profile
  posts: [Post!]!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Profile {
  id: ID!
  firstName: String
  lastName: String
  bio: String
  avatar: URL
  user: User!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  tags: [Tag!]!
  publishedAt: DateTime
  isPublished: Boolean!
  comments: [Comment!]!
}

枚举类型(Enum Types):

enum UserRole {
  ADMIN
  MODERATOR
  USER
  GUEST
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

enum SortOrder {
  ASC
  DESC
}

接口类型(Interface Types):

interface Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}

interface Timestamped {
  createdAt: DateTime!
  updatedAt: DateTime!
}

type User implements Node & Timestamped {
  id: ID!
  username: String!
  email: Email!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Post implements Node & Timestamped {
  id: ID!
  title: String!
  content: String!
  createdAt: DateTime!
  updatedAt: DateTime!
}

联合类型(Union Types):

union SearchResult = User | Post | Comment

type Query {
  search(query: String!): [SearchResult!]!
}

输入类型(Input Types)

input CreateUserInput {
  username: String!
  email: Email!
  password: String!
  profile: CreateProfileInput
}

input CreateProfileInput {
  firstName: String
  lastName: String
  bio: String
}

input UpdateUserInput {
  username: String
  email: Email
  profile: UpdateProfileInput
}

input UpdateProfileInput {
  firstName: String
  lastName: String
  bio: String
  avatar: String
}

input UserFilters {
  role: UserRole
  isActive: Boolean
  createdAfter: DateTime
  createdBefore: DateTime
}

input PaginationInput {
  first: Int
  after: String
  last: Int
  before: String
}

根类型定义

# 查询根类型
type Query {
  # 单个资源查询
  user(id: ID!): User
  post(id: ID!): Post
  
  # 列表查询
  users(
    filters: UserFilters
    pagination: PaginationInput
    sort: UserSortInput
  ): UserConnection!
  
  posts(
    filters: PostFilters
    pagination: PaginationInput
  ): PostConnection!
  
  # 搜索
  search(query: String!, type: SearchType): [SearchResult!]!
  
  # 当前用户
  me: User
}

# 变更根类型
type Mutation {
  # 用户操作
  createUser(input: CreateUserInput!): CreateUserPayload!
  updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
  deleteUser(id: ID!): DeleteUserPayload!
  
  # 文章操作
  createPost(input: CreatePostInput!): CreatePostPayload!
  updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
  deletePost(id: ID!): DeletePostPayload!
  publishPost(id: ID!): PublishPostPayload!
  
  # 认证操作
  login(email: String!, password: String!): AuthPayload!
  logout: LogoutPayload!
  refreshToken(token: String!): AuthPayload!
}

# 订阅根类型
type Subscription {
  # 用户相关订阅
  userCreated: User!
  userUpdated(id: ID!): User!
  
  # 文章相关订阅
  postPublished: Post!
  postUpdated(authorId: ID!): Post!
  
  # 评论相关订阅
  commentAdded(postId: ID!): Comment!
}

连接和分页模式

# Relay-style连接
type UserConnection {
  edges: [UserEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type UserEdge {
  node: User!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

# 简单分页
type UserList {
  users: [User!]!
  pagination: PaginationInfo!
}

type PaginationInfo {
  page: Int!
  limit: Int!
  total: Int!
  totalPages: Int!
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
}

错误处理模式

# 统一的错误处理
interface MutationPayload {
  success: Boolean!
  message: String
  errors: [UserError!]
}

type UserError {
  field: String
  message: String!
  code: String
}

type CreateUserPayload implements MutationPayload {
  success: Boolean!
  message: String
  errors: [UserError!]
  user: User
}

type UpdateUserPayload implements MutationPayload {
  success: Boolean!
  message: String
  errors: [UserError!]
  user: User
}

# 结果联合类型
union CreateUserResult = CreateUserSuccess | ValidationError | DuplicateError

type CreateUserSuccess {
  user: User!
}

type ValidationError {
  field: String!
  message: String!
}

type DuplicateError {
  field: String!
  value: String!
  message: String!
}

指令(Directives)

# 内置指令
type User {
  id: ID!
  username: String!
  email: String! @deprecated(reason: "Use profile.email instead")
  secretField: String @skip(if: $skipSecret)
  adminField: String @include(if: $isAdmin)
}

# 自定义指令
directive @auth(role: UserRole = USER) on FIELD_DEFINITION
directive @rate_limit(max: Int!, duration: Int!) on FIELD_DEFINITION
directive @cache(ttl: Int!) on FIELD_DEFINITION

type Query {
  users: [User!]! @auth(role: ADMIN) @rate_limit(max: 100, duration: 60)
  me: User @auth @cache(ttl: 300)
}

实战经验

Schema 设计最佳实践

1. 命名约定:

# 使用 PascalCase 命名类型
type UserProfile {
  id: ID!
}

# 使用 camelCase 命名字段
type User {
  firstName: String!
  lastName: String!
  createdAt: DateTime!
}

# 使用 SCREAMING_SNAKE_CASE 命名枚举值
enum UserStatus {
  ACTIVE
  INACTIVE
  SUSPENDED
  PENDING_VERIFICATION
}

2. 可空性设计:

type User {
  # 必需字段使用 ! 
  id: ID!
  username: String!
  email: String!
  
  # 可选字段不使用 !
  profile: Profile
  lastLoginAt: DateTime
  
  # 列表字段的可空性设计
  posts: [Post!]!      # 列表不为空,元素不为空
  tags: [String!]      # 列表可为空,元素不为空(如果列表存在)
  metadata: [String]   # 列表和元素都可为空
}

3. 字段设计原则:

# 避免过度嵌套
type User {
  id: ID!
  profile: Profile!
  # 避免这样的深层嵌套
  # profile: { personal: { address: { country: { code } } } }
}

# 提供合理的默认值
type Query {
  users(
    first: Int = 10
    orderBy: UserOrderBy = CREATED_AT
    direction: SortDirection = DESC
  ): [User!]!
}

# 使用描述性的字段名
type Post {
  publishedAt: DateTime    # 而不是 date
  isPublished: Boolean     # 而不是 status
  viewCount: Int          # 而不是 views
}

模块化 Schema 设计

1. 按领域分割:

# user.graphql
type User {
  id: ID!
  username: String!
  email: String!
}

extend type Query {
  user(id: ID!): User
  users: [User!]!
}

extend type Mutation {
  createUser(input: CreateUserInput!): User!
}

# post.graphql
type Post {
  id: ID!
  title: String!
  author: User!
}

extend type Query {
  post(id: ID!): Post
  posts: [Post!]!
}

extend type Mutation {
  createPost(input: CreatePostInput!): Post!
}

# 在用户类型上扩展文章字段
extend type User {
  posts: [Post!]!
}

2. 使用 Schema 拼接:

// schema/index.js
import { makeExecutableSchema } from '@graphql-tools/schema';
import { mergeTypeDefs } from '@graphql-tools/merge';
import userTypeDefs from './user.graphql';
import postTypeDefs from './post.graphql';
import baseTypeDefs from './base.graphql';

const typeDefs = mergeTypeDefs([
  baseTypeDefs,
  userTypeDefs,
  postTypeDefs
]);

export const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

性能优化 Schema 设计

1. N+1 查询问题解决:

type Post {
  id: ID!
  title: String!
  author: User!     # 可能导致 N+1 查询
  comments: [Comment!]!  # 可能导致 N+1 查询
}

# 解决方案:使用 DataLoader
type Query {
  posts: [Post!]!
}
// 使用 DataLoader 解决 N+1 问题
import DataLoader from 'dataloader';

const userLoader = new DataLoader(async (userIds) => {
  const users = await User.findByIds(userIds);
  return userIds.map(id => users.find(user => user.id === id));
});

const resolvers = {
  Post: {
    author: (post) => userLoader.load(post.authorId)
  }
};

2. 分页和限制:

type Query {
  posts(
    first: Int = 10
    after: String
    # 强制限制最大请求数量
  ): PostConnection! @constraint(maxFirst: 100)
}

directive @constraint(
  maxFirst: Int
) on FIELD_DEFINITION

版本控制策略

1. 字段弃用:

type User {
  id: ID!
  username: String!
  email: String! @deprecated(reason: "Use profile.email instead")
  profile: Profile!
}

type Profile {
  email: String!
  # 新的邮箱字段位置
}

2. 渐进式迁移:

# 旧版本字段保持向后兼容
type User {
  id: ID!
  name: String! @deprecated(reason: "Use firstName and lastName instead")
  firstName: String!
  lastName: String!
}

安全考虑

1. 查询深度限制:

import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(10)]
});

2. 查询复杂度分析:

import costAnalysis from 'graphql-cost-analysis';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    costAnalysis({
      maximumCost: 1000,
      defaultCost: 1,
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10
    })
  ]
});

3. 授权指令:

directive @auth(role: UserRole) on FIELD_DEFINITION

type Query {
  adminUsers: [User!]! @auth(role: ADMIN)
  me: User @auth
}

经验总结

优势

  • 强类型系统:编译时类型检查,减少运行时错误
  • 自文档化:Schema 即文档,自动生成 API 文档
  • 灵活查询:客户端可按需请求数据
  • 工具生态:丰富的开发工具支持
  • 版本演进:通过字段弃用实现向后兼容

Schema 设计原则

  1. 以客户端需求为导向:设计 Schema 时考虑客户端的实际使用场景
  2. 保持简单性:避免过度复杂的嵌套和抽象
  3. 一致性:保持命名约定和设计模式的一致性
  4. 可扩展性:设计时考虑未来的扩展需求
  5. 性能考虑:避免可能导致性能问题的设计

常见问题与解决方案

  1. 过度获取数据

    • 使用细粒度的字段设计
    • 提供合理的默认查询
  2. 查询复杂度过高

    • 实施查询复杂度分析
    • 设置查询深度限制
  3. N+1 查询问题

    • 使用 DataLoader 模式
    • 合理设计批量查询接口
  4. Schema 演进困难

    • 使用字段弃用而非删除
    • 保持向后兼容性

适用场景

适合使用 GraphQL Schema:

  • 复杂的数据关系
  • 多样化的客户端需求
  • 需要灵活查询的场景
  • 快速迭代的产品

不适合使用 GraphQL:

  • 简单的 CRUD 操作
  • 文件上传下载
  • 实时性要求极高的场景
  • 缓存策略复杂的场景

信息参考

创建于 2025/1/1 更新于 2026/5/27