GraphQL 服务端
问题
GraphQL 服务端如何设计 Schema 和 Resolver?N+1 问题怎么解决?
答案
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 怎么选?
答案:
| 维度 | REST | GraphQL |
|---|---|---|
| 数据获取 | 固定结构 | 按需获取 |
| 过度获取 | 常见 | 不存在 |
| 缓存 | HTTP 缓存天然支持 | 需要额外工具 |
| 学习曲线 | 低 | 中 |
| 适用场景 | CRUD 为主 | 复杂关联数据、多端 |
Q2: GraphQL 的安全问题?
答案:
- 查询深度限制:防止恶意嵌套查询
- 查询复杂度限制:给 field 设置 cost
- 速率限制:按用户/IP 限制
- 关闭 introspection:生产环境禁用 Schema 自省
Q3: GraphQL 的缓存怎么做?
答案:
- 客户端:Apollo Client 自动基于
__typename+id做归一化缓存 - 服务端:Persisted Queries(预编译查询)、CDN 缓存 GET 请求