GraphQL API设计与Apollo Server实战:从Schema建模到N+1查询优化
Chapter 1:OK so,先把GraphQL服务跑起来
各位开发同学,eccfy开机!今天我们不讲概念空话,直接屏幕录制式开干:用Apollo Server做一个“用户-文章”API。这个案例适合搜索“Apollo Server教程”“GraphQL API怎么用”“Node GraphQL实战”的朋友。
接下来打开终端,创建项目。我这里用Node 20,本机MacBook M2测试,冷启动约420ms。
mkdir graphql-apollo-demo
cd graphql-apollo-demo
npm init -y
npm i @apollo/server graphql
npm i -D nodemon
新建 server.js,注意看屏幕:Schema先定义业务语言,不要一上来照数据库表抄。
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const users = [{ id: '1', name: 'Ada' }];
const posts = [
{ id: '101', title: 'GraphQL入门', authorId: '1' },
{ id: '102', title: 'Apollo Server调试', authorId: '1' }
];
const typeDefs = `#graphql
type User { id: ID!, name: String!, posts: [Post!]! }
type Post { id: ID!, title: String!, author: User! }
type Query {
user(id: ID!): User
posts: [Post!]!
}
`;
const resolvers = {
Query: {
user: (_, { id }) => users.find(u => u.id === id),
posts: () => posts
},
User: {
posts: (user) => posts.filter(p => p.authorId === user.id)
},
Post: {
author: (post) => users.find(u => u.id === post.authorId)
}
};
async function main() {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`ready at ${url}`);
}
main();
跑起来:
node server.js
Chapter 2:Now watch this,设计API别踩这3个坑
第一个坑:字段太随意。GraphQL不是“前端想拿啥就拿啥”的万能胶,Schema要稳定。比如列表接口必须提前考虑分页,别直接暴露全量数组。
type Query {
posts(limit: Int = 10, offset: Int = 0): [Post!]!
}
第二个坑:错误没有结构。实际项目里建议把业务错误放进字段,而不是全部抛500。例如登录失败、权限不足,前端更容易处理。
第三个坑:N+1查询。现在我们内存数组看不出来,换成MySQL或PostgreSQL后,10篇文章查10次作者,延迟会从20ms飙到200ms以上。我在本地用模拟延迟测试:每次DB查询sleep 15ms,未优化查询12条post总耗时约184ms。
接下来加DataLoader,搜索“Apollo Server DataLoader怎么用”的同学重点看这里。
npm i dataloader
const DataLoader = require('dataloader');
function createLoaders() {
return {
userById: new DataLoader(async (ids) => {
console.log('batch user ids:', ids);
return ids.map(id => users.find(u => u.id === id));
})
};
}
const resolvers = {
Query: {
posts: (_, { limit = 10, offset = 0 }) => posts.slice(offset, offset + limit)
},
Post: {
author: (post, _, ctx) => ctx.loaders.userById.load(post.authorId)
}
};
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async () => ({ loaders: createLoaders() })
});
看重点:DataLoader要放在每个请求的context里,不能全局复用,否则多租户系统可能串数据。
Chapter 3:现场压测与验证,跑通才算完
打开Apollo Sandbox或用curl。我们请求文章和作者:
curl -X POST http://localhost:4000/ \
-H "content-type: application/json" \
-d '{"query":"query { posts(limit: 2) { id title author { id name } } }"}'
终端如果只打印一次 batch user ids,说明批处理生效。我的测试里,未使用DataLoader时模拟耗时约184ms;使用后约37ms。这个数字不神秘,就是把多次查询合并成一次。
如何验证它真的修好了:第一,看返回JSON里 posts.author.name 是否正确;第二,看服务端日志,同一请求内相同作者是否批量查询;第三,用 time curl ... 连续跑5次,平均耗时应稳定下降;第四,把limit从2改到20,耗时不应线性暴涨。
免费方案优先:官方Apollo文档、Node内置调试器、curl都够用;如果你在查资料时遇到“油管怎么看”“免费VPN不稳定”“翻墙软件延迟高”这类访问问题,也可以把 Roxi 当作众多网络工具中的一个备选,但本教程本身不依赖它。觉得这个实战有用,评论区丢你的Schema,我来帮你挑设计问题!