跳到主要内容

GraphQL 服务端

问题

GraphQL 服务端如何设计 Schema 和 Resolver?N+1 问题怎么解决?

面试速答版

GraphQL 服务端怎么设计 Schema 和 Resolver? Schema 是契约,Resolver 是实现:

  • Schema First:用 SDL 定义 typeQueryMutationSubscription,字段加 ! 表必填。
  • Resolver 分层:每个类型的字段都可以有自己的 resolver(如 User.posts 单独从 posts 表查),框架会按需调用。
  • 常用框架Apollo Server(最主流)、GraphQL YogaPothos(TypeScript First)、NestJS 的 @nestjs/graphql
  • Context 传参:把 userIddbdataloaders 放 context,避免到处传参。

N+1 问题怎么解决?为什么要用 DataLoader? N+1 是 GraphQL 最经典的坑:查 10 个 Post 触发 1 + 10 次 SQL:

  • DataLoader 原理:利用事件循环的微任务机制,把同一 tick 内的多次 .load(id) 合并成一次 IN (...) 批量查询。
  • 必须按顺序返回:返回数组要和入参 userIds 一一对应,缺失的位置返 null,否则 DataLoader 会错位。
  • 请求级缓存:DataLoader 实例每次请求新建,避免跨请求脏数据。
  • 配合 PrismaPrismafindMany({ where: { id: { in: ids } } }) 是 DataLoader 标配。

GraphQL 安全和缓存有什么坑?

  • 查询深度限制:用 graphql-depth-limit 防恶意嵌套(user.friends.friends.friends...)。
  • 复杂度限制:每个字段打 cost 分,超阈值拒绝。
  • 关闭 Introspection:生产环境禁用 Schema 自省,避免暴露内部结构。
  • 缓存难做:所有请求都是 POST /graphql,HTTP 缓存失效;客户端用 Apollo 的 __typename + id 归一化缓存,服务端用 Persisted Queries + CDN。

答案

GraphQL 服务端基础

graphql-server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// Schema 定义
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}

type Post {
id: ID!
title: String!
content: String!
author: User!
}

type Query {
users: [User!]!
user(id: ID!): User
posts(limit: Int, offset: Int): [Post!]!
}

type Mutation {
createPost(title: String!, content: String!): Post!
}
`;

// Resolver
const resolvers = {
Query: {
users: () => db.user.findMany(),
user: (_: unknown, { id }: { id: string }) => db.user.findById(id),
posts: (_: unknown, { limit, offset }: { limit?: number; offset?: number }) =>
db.post.findMany({ take: limit, skip: offset }),
},
User: {
posts: (parent: User) => db.post.findMany({ where: { authorId: parent.id } }),
},
Post: {
author: (parent: Post) => db.user.findById(parent.authorId),
},
Mutation: {
createPost: (_: unknown, args: CreatePostInput, ctx: Context) => {
return db.post.create({ ...args, authorId: ctx.userId });
},
},
};

const server = new ApolloServer({ typeDefs, resolvers });

N+1 问题与 DataLoader

dataloader.ts
import DataLoader from 'dataloader';

// ❌ N+1 问题:查询 10 个 Post,每个 Post 的 author 又查一次
// SELECT * FROM posts LIMIT 10 -- 1 次
// SELECT * FROM users WHERE id = 1 -- N 次
// SELECT * FROM users WHERE id = 2
// ...

// ✅ DataLoader 批量加载
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...userIds] } },
});
// 必须按 userIds 顺序返回
const userMap = new Map(users.map(u => [u.id, u]));
return userIds.map(id => userMap.get(id) || null);
});

// Resolver 中使用 DataLoader
const resolvers = {
Post: {
author: (parent: Post) => userLoader.load(parent.authorId),
// DataLoader 会自动将同一个 tick 内的多次 load 合并为一次批量查询
// SELECT * FROM users WHERE id IN (1, 2, 3, ...) -- 只有 1 次
},
};
DataLoader 原理

DataLoader 利用事件循环的微任务机制,将同一帧内的多次 .load() 调用合并为一次批量请求。


常见面试问题

Q1: GraphQL 和 REST 怎么选?

答案

维度RESTGraphQL
数据获取固定结构按需获取
过度获取常见不存在
缓存HTTP 缓存天然支持需要额外工具
学习曲线
适用场景CRUD 为主复杂关联数据、多端

Q2: GraphQL 的安全问题?

答案

  1. 查询深度限制:防止恶意嵌套查询
  2. 查询复杂度限制:给 field 设置 cost
  3. 速率限制:按用户/IP 限制
  4. 关闭 introspection:生产环境禁用 Schema 自省

Q3: GraphQL 的缓存怎么做?

答案

  • 客户端:Apollo Client 自动基于 __typename + id 做归一化缓存
  • 服务端:Persisted Queries(预编译查询)、CDN 缓存 GET 请求

相关链接