GraphQL 入门
大约 9 分钟约 2641 字
GraphQL 入门
简介
GraphQL 是一种 API 查询语言和运行时协议,核心价值在于:客户端只拿自己真正需要的数据。相比 REST 多个端点分散组织资源,GraphQL 更强调围绕数据关系构建统一 Schema,适合前后端协作复杂、页面聚合需求强、字段裁剪要求高的场景。
特点
实现
Schema 与核心概念
# 用户类型
type User {
id: ID!
name: String!
email: String!
role: Role!
posts: [Post!]!
createdAt: DateTime!
}
# 文章类型
type Post {
id: ID!
title: String!
content: String!
tags: [String!]!
author: User!
createdAt: DateTime!
}
enum Role {
ADMIN
USER
GUEST
}
type Query {
user(id: ID!): User
users(page: Int = 1, size: Int = 20, role: Role): UserPage!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
createPost(input: CreatePostInput!): Post!
}
type Subscription {
postCreated: Post!
}
input CreateUserInput {
name: String!
email: String!
role: Role = USER
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
type UserPage {
items: [User!]!
total: Int!
page: Int!
size: Int!
}GraphQL 三个核心动作:
- Query:读数据
- Mutation:改数据
- Subscription:订阅实时变化GraphQL 和 REST 的核心差别:
- REST:服务端定义返回结构
- GraphQL:客户端声明返回结构查询、变量与片段
query GetUserDetail($userId: ID!) {
user(id: $userId) {
id
name
email
role
posts {
id
title
tags
}
}
}{
"userId": "1"
}# 列表查询 + 分页
query GetUsers($page: Int!, $size: Int!) {
users(page: $page, size: $size) {
items {
id
name
email
}
total
page
size
}
}# Fragment 复用字段
fragment UserBasicInfo on User {
id
name
email
}
query GetUserAndPost($userId: ID!, $postId: ID!) {
user(id: $userId) {
...UserBasicInfo
}
post(id: $postId) {
id
title
author {
...UserBasicInfo
}
}
}Mutation 与错误处理思路
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}{
"input": {
"name": "张三",
"email": "zhangsan@example.com",
"role": "USER"
}
}{
"data": null,
"errors": [
{
"message": "邮箱已存在",
"path": ["createUser"],
"extensions": {
"code": "EMAIL_EXISTS"
}
}
]
}GraphQL 错误响应通常不是 HTTP 4xx/5xx 直接表达业务错误,
而是通过 errors 数组返回。
所以前端要同时处理:
- transport error
- GraphQL errors
- data partial nullDataLoader 解决 N+1 问题
N+1 是 GraphQL 最常见的性能陷阱。当一个查询返回 N 条记录,而每条记录又需要单独查询关联数据时,就会产生 N+1 问题。DataLoader 通过批量化和缓存机制解决这个问题。
// 问题场景:查询所有用户及其文章
// 没有 DataLoader:1 次查用户 + N 次查文章 = N+1 次数据库查询
// 1. 定义 DataLoader
public class PostByAuthorIdDataLoader : BatchDataLoader<int, List<Post>>
{
private readonly IPostService _postService;
public PostByAuthorIdDataLoader(
IPostService postService,
IBatchScheduler batchScheduler,
DataLoaderOptions? options = null)
: base(batchScheduler, options)
{
_postService = postService;
}
protected override async Task<IReadOnlyDictionary<int, List<Post>>> LoadBatchAsync(
IReadOnlyList<int> keys,
CancellationToken cancellationToken)
{
// 一次性查询所有作者的帖子,而不是逐个查询
var posts = await _postService.GetByAuthorIdsAsync(keys, cancellationToken);
return posts
.GroupBy(p => p.AuthorId)
.ToDictionary(g => g.Key, g => g.ToList());
}
}
// 2. 在 Resolver 中使用 DataLoader
public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor.Field(u => u.Posts)
.Resolve(async (ctx, ct) =>
{
var user = ctx.Parent<User>();
var loader = ctx.DataLoader<PostByAuthorIdDataLoader>();
return await loader.LoadAsync(user.Id, ct);
});
}
}
// 3. 注册 DataLoader
builder.Services.AddDataLoader<PostByAuthorIdDataLoader>();DataLoader 工作原理:
1. 收集阶段:在同一个请求中,所有对同一 DataLoader 的调用会收集 key
2. 批处理阶段:将收集到的 key 一次性传递给 LoadBatchAsync
3. 分发阶段:将批量查询结果按 key 分发给各个调用方
4. 缓存:同一请求内,相同 key 只查询一次查询复杂度与深度限制
生产环境中必须限制查询的复杂度和深度,防止恶意或低效查询消耗过多资源。
// Hot Chocolate 配置查询复杂度限制
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddMaxExecutionDepthRule(10) // 最大嵌套深度
.AddCostMeasurements() // 启用成本计算
.ModifyCostOptions(o =>
{
o.DefaultFieldCost = 1; // 每个字段默认成本
o.MaxCost = 500; // 最大允许成本
});
// 为列表字段设置自定义成本
public class Query
{
[Cost(5)] // 列表查询成本更高
public async Task<UserPage> GetUsersAsync(
int page, int size,
[Service] IUserService userService,
CancellationToken cancellationToken)
{
return await userService.GetPageAsync(page, size, null, cancellationToken);
}
}
// 查询复杂度分析示例
// 简单查询:成本 = 2(user + name)
// { user(id:1) { name } }
//
// 嵌套查询:成本 = 4 + 5*20 = 104(users 成本 5 * 20 条 + 每条的 name)
// { users(page:1, size:20) { items { name posts { title } } } }// 持久化查询(Persisted Queries)— 只允许预注册的查询
// 适合生产环境,防止任意查询执行
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.UsePersistedQueryPipeline()
.AddFileSystemQueryStorage("./graphql-queries");
// 客户端使用持久化查询
// POST /graphql
// {
// "id": "GetUserDetail",
// "variables": { "userId": "1" }
// }Hot Chocolate 服务端集成
using HotChocolate;
using HotChocolate.Subscriptions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IPostService, PostService>();
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddSubscriptionType<Subscription>()
.AddInMemorySubscriptions();
var app = builder.Build();
app.UseWebSockets();
app.MapGraphQL("/graphql");
app.Run();public class Query
{
public async Task<User?> GetUserAsync(
int id,
[Service] IUserService userService,
CancellationToken cancellationToken)
{
return await userService.GetByIdAsync(id, cancellationToken);
}
public async Task<UserPage> GetUsersAsync(
int page,
int size,
Role? role,
[Service] IUserService userService,
CancellationToken cancellationToken)
{
return await userService.GetPageAsync(page, size, role, cancellationToken);
}
}public class Mutation
{
public async Task<User> CreateUserAsync(
CreateUserInput input,
[Service] IUserService userService,
CancellationToken cancellationToken)
{
return await userService.CreateAsync(input, cancellationToken);
}
public async Task<Post> CreatePostAsync(
CreatePostInput input,
[Service] IPostService postService,
[Service] ITopicEventSender eventSender,
CancellationToken cancellationToken)
{
var post = await postService.CreateAsync(input, cancellationToken);
await eventSender.SendAsync(nameof(Subscription.PostCreated), post, cancellationToken);
return post;
}
}public class Subscription
{
[Subscribe]
[Topic]
public Post PostCreated([EventMessage] Post post) => post;
}前端调用示例
const query = `
query GetUsers($page: Int!, $size: Int!) {
users(page: $page, size: $size) {
items {
id
name
email
}
total
page
size
}
}
`
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query,
variables: { page: 1, size: 10 }
})
})
const result = await response.json()
console.log(result)// Apollo Client 示例(简化)
import { ApolloClient, InMemoryCache, gql } from '@apollo/client'
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache()
})
const { data } = await client.query({
query: gql`
query {
user(id: 1) {
id
name
email
}
}
`
})GraphQL 安全与权限控制
字段级权限控制
// 基于角色的字段级授权
public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor.Field(u => u.Email)
.Authorize("AdminPolicy"); // 只有 Admin 能查看邮箱
descriptor.Field(u => u.Role)
.Authorize("AdminPolicy");
descriptor.Field(u => u.Id).AllowAnonymous();
descriptor.Field(u => u.Name).AllowAnonymous();
}
}
// 定义授权策略
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminPolicy", policy =>
policy.RequireRole("ADMIN"));
});
// 在 Mutation 上要求认证
public class Mutation
{
[Authorize]
public async Task<Post> CreatePostAsync(
CreatePostInput input,
[Service] IPostService postService,
CancellationToken cancellationToken)
{
return await postService.CreateAsync(input, cancellationToken);
}
[Authorize(Policy = "AdminPolicy")]
public async Task<bool> DeleteUserAsync(
int id,
[Service] IUserService userService,
CancellationToken cancellationToken)
{
return await userService.DeleteAsync(id, cancellationToken);
}
}查询防护与限流
// 查询超时控制
builder.Services
.AddGraphQLServer()
.ModifyRequestOptions(o =>
{
o.ExecutionTimeout = TimeSpan.FromSeconds(30);
});
// 基于请求频率的限流
public class GraphQLRateLimitMiddleware
{
private readonly RequestDelegate _next;
private static readonly ConcurrentDictionary<string, Queue<DateTime>> _requestLog = new();
public GraphQLRateLimitMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var clientId = context.User?.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString() ?? "anonymous";
var now = DateTime.UtcNow;
var window = TimeSpan.FromMinutes(1);
var maxRequests = 60;
var queue = _requestLog.GetOrAdd(clientId, _ => new Queue<DateTime>());
lock (queue)
{
while (queue.Count > 0 && now - queue.Peek() > window)
queue.Dequeue();
if (queue.Count >= maxRequests)
{
context.Response.StatusCode = 429;
return;
}
queue.Enqueue(now);
}
await _next(context);
}
}错误处理与自定义错误过滤器
// 自定义错误过滤器,统一错误格式
public class GraphQLErrorFilter : IErrorFilter
{
public IError OnError(IError error)
{
// 隐藏内部异常详情,避免信息泄露
var message = error.Exception switch
{
NotFoundException => "请求的资源不存在",
ValidationException => error.Exception.Message,
UnauthorizedException => "权限不足",
_ => "服务器内部错误"
};
return error
.WithMessage(message)
.WithCode(error.Exception switch
{
NotFoundException => "NOT_FOUND",
ValidationException => "VALIDATION_ERROR",
UnauthorizedException => "FORBIDDEN",
_ => "INTERNAL_ERROR"
})
.RemoveExtensions()
.RemoveLocations();
}
}
// 注册错误过滤器
builder.Services
.AddGraphQLServer()
.AddErrorFilter<GraphQLErrorFilter>();优点
缺点
总结
GraphQL 最有价值的地方,不是“语法新”,而是它很适合复杂页面和聚合型查询场景。真正落地时,重点不只是会写 Schema,而是要把权限、数据加载、缓存、错误处理和前后端协作方式一起设计清楚。
关键知识点
- Query / Mutation / Subscription 是 GraphQL 三种核心能力。
- Schema 是契约中心,不能把 GraphQL 当成“随便查数据库”的自由入口。
- GraphQL 前端很灵活,服务端也更容易遇到 N+1 问题。
- 错误处理、权限控制和字段裁剪是生产使用的核心边界。
项目落地视角
- 管理后台详情页、聚合仪表盘、BFF 层特别适合 GraphQL。
- 移动端、前端多页面复用同一数据模型时也很受益。
- 微服务内部仍未必适合全改 GraphQL,通常更适合放在边缘聚合层。
- 强权限系统里,字段级授权和查询复杂度限制非常重要。
常见误区
- 把 GraphQL 当成“万能 API”,最后所有查询都无限自由。
- 不做 DataLoader,导致经典 N+1 查询问题。
- 只关注前端拿数方便,不关心服务端成本和安全边界。
- 以为单一端点就会天然更简单,实际上服务端治理难度会上升。
进阶路线
- 学习 DataLoader、查询复杂度限制、持久化查询(Persisted Query)。
- 深入 Hot Chocolate / Apollo Federation / Schema Stitching。
- 在 BFF 层实践 GraphQL 聚合而不是盲目替代全部 REST。
- 研究字段级权限控制、缓存策略和订阅体系。
适用场景
- 聚合型前端页面。
- 管理后台详情页和复杂报表页面。
- 前端多端共用同一数据模型的系统。
- 需要统一数据契约和高协作效率的团队。
落地建议
- 从最适合的聚合场景试点,不要一开始全站替换 REST。
- 先设计 Schema,再设计 resolver,不要把数据库结构直接暴露出去。
- 对高频查询建立性能分析和复杂度限制。
- 把权限、分页、错误码、缓存策略一起纳入 API 设计。
排错清单
- 查询慢时先检查是否出现 N+1。
- 返回字段异常时检查 Schema、resolver 和序列化映射是否一致。
- 前端拿不到数据时检查 errors 数组,而不只看 HTTP 状态码。
- 权限相关 bug 时检查字段级授权和 resolver 层校验。
复盘问题
- 你的场景是真需要 GraphQL,还是 REST 其实已经足够?
- 当前问题更像是数据聚合问题,还是接口设计问题?
- GraphQL 给前端带来的灵活性,是否值得服务端治理复杂度?
- 如果查询变得越来越复杂,你有足够的限流和复杂度控制吗?
