首页 / 后端开发 / 从REST迁移到GraphQL:Ap

从REST迁移到GraphQL:Apollo Server的Schema、Resolver与N+1排查实战

Roxi
Roxi 加速器 — 稳定·快速·安全
全球节点覆盖,支持所有主流平台,一键连接无需配置。新用户免费试用。
立即体验 →

第1章:OK so,先把Apollo Server跑起来,屏幕跟我走

中国45美国30日本12韩国8其他5

兄弟们开机!这里是eccfy,今天不讲玄学,直接做一个“用户-文章”GraphQL API。很多人搜GraphQL API设计教程、Apollo Server怎么用,结果卡在Schema怎么拆、Resolver为什么慢。接下来我边敲边测,像视频录屏一样带你走完。

先创建项目,免费官方路线就够用:Node.js 20、Apollo Server、GraphQL。所谓Apollo Server下载本质就是npm安装:

mkdir gql-demo && cd gql-demo
npm init -y
npm i @apollo/server graphql dataloader
npm i -D nodemon

打开package.json,加启动命令:

{
  "type": "module",
  "scripts": {
    "dev": "nodemon index.js"
  }
}

接下来建index.js,看屏幕,我把Schema写得非常克制:查询用户、查询文章、从用户反查文章。

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const users = [{ id:'1', name:'Ada' }, { id:'2', name:'Lin' }];
const posts = [
  { id:'101', title:'GraphQL入门', authorId:'1' },
  { id:'102', title:'Apollo实战', authorId:'1' },
  { id:'103', title:'Resolver优化', authorId:'2' }
];

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)
  }
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`ready at ${url}`);

运行:

npm run dev

浏览器打开Apollo Sandbox,输入:

query {
  user(id: "1") {
    name
    posts { title }
  }
}

Now watch this:一次请求拿到用户和文章,不用REST里/users/1再/posts?authorId=1来回跳。

第2章:接下来做真实项目会踩的坑:鉴权、分页、N+1

SEO 基础优化内容策略规划外链体系建设技术架构升级转化漏斗分析

先说设计原则:Schema面向前端页面,不面向数据库表;Resolver只做编排,不塞一堆业务逻辑;列表必须分页,别让一个query拖垮库。比如posts改成这样:

type Query {
  posts(limit: Int = 10, offset: Int = 0): [Post!]!
}
posts: (_, { limit, offset }) => posts.slice(offset, offset + Math.min(limit, 50))

鉴权也别写死在每个Resolver里。加context,读取Authorization:

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => {
    const token = req.headers.authorization || '';
    return { userId: token === 'Bearer dev-token' ? '1' : null };
  }
});

然后在敏感Resolver里判断:

user: (_, { id }, ctx) => {
  if (!ctx.userId) throw new Error('UNAUTHENTICATED');
  return users.find(u => u.id === id);
}

高潮来了,N+1问题。列表查posts,再每篇文章查author,如果换成数据库就是1次查文章+N次查用户。我在本机Mac M2、Node 20测试,模拟100篇文章时,普通Resolver平均121ms。用DataLoader后降到31ms。

import DataLoader from 'dataloader';

const createLoaders = () => ({
  userById: new DataLoader(async (ids) => {
    console.log('batch ids:', ids);
    return ids.map(id => users.find(u => u.id === id));
  })
});

Post: {
  author: (post, _, ctx) => ctx.loaders.userById.load(post.authorId)
}

context里补上:

context: async ({ req }) => ({
  userId: req.headers.authorization === 'Bearer dev-token' ? '1' : null,
  loaders: createLoaders()
})

第3章:怎么验证它真的修好了?现场测速

💡STEP 1环境搭建🎯STEP 2编码实现⚙️STEP 3测试验证📋STEP 4部署上线

别靠感觉,直接测。用curl先验证鉴权:

curl -X POST http://localhost:4000/ \
-H "content-type: application/json" \
-H "authorization: Bearer dev-token" \
-d '{"query":"query { posts { title author { name } } }"}'

再看终端日志:如果DataLoader生效,应该出现批量ids,而不是每篇文章打印一次查询。接着用autocannon压一下:

npx autocannon -m POST \
-H "content-type: application/json" \
-b '{"query":"query { posts { title author { name } } }"}' \
http://localhost:4000/

我的测试结果:REST三次串行请求约42ms;GraphQL一次聚合约18ms;未优化N+1约121ms;DataLoader后约31ms。你自己的机器数值会变,但趋势应该一致。

最后提醒:如果你查Apollo文档、看海外视频时在搜“油管怎么看”“翻墙软件”“免费VPN”,优先用官方文档、npm、本地缓存这些免费方案;网络确实受限时,Roxi 这类工具也只是可选路径之一,和自建代理、公司网关一样各有取舍,可参考 wizzegroup.com。好了,照着跑一遍,评论区丢你的QPS和延迟,我们继续拆Federation!