本项目基于 Spring for GraphQL 实现了声明式按需加载的数据分片加载方案,通过 Fragments 和 DataLoader 技术,实现了高效的数据查询和传输。
- ✅ 官方支持:Spring 官方推荐的 GraphQL 解决方案
- ✅ 声明式编程:通过
@SchemaMapping声明式定义字段解析 - ✅ Fragments 原生支持:前端通过 Fragments 精确控制数据需求
- ✅ DataLoader 集成:自动批量加载,解决 N+1 查询问题
- ✅ 类型安全:基于 Schema 定义,提供完整的类型检查
| 特性 | REST API | GraphQL |
|---|---|---|
| 数据获取 | 多个端点,可能过度获取 | 单一端点,按需获取 |
| 版本管理 | 需要维护多个版本 | 无需版本,持续演进 |
| N+1 问题 | 容易出现,需手动优化 | DataLoader 自动解决 |
| 文档维护 | 需手动编写 | Schema 即文档 |
| 前端灵活性 | 受后端 API 限制 | 前端自由组合字段 |
┌─────────────────────────────────────────────────────────────┐
│ 前端 Vue 3 │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ 列表页组件 │ │ 详情页组件 │ │ 管理后台 │ │
│ │ (基础字段) │ │ (完整内容) │ │ (Token审计) │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ↓ │
│ GraphQL Fragments │
│ (定义不同场景的字段组合) │
└──────────────────────────────┬──────────────────────────────┘
↓
HTTP POST /graphql
↓
┌─────────────────────────────────────────────────────────────┐
│ Spring for GraphQL │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GraphQL Schema (schema.graphqls) │ │
│ │ - 定义类型、查询、变更 │ │
│ │ - 声明式字段定义 │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GraphQL Controller (@SchemaMapping) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ │
│ │ │ @QueryMapping│ │@SchemaMapping│ │DataLoader│ │ │
│ │ │ (入口查询) │ │ (字段解析) │ │(批量加载)│ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └─────┬────┘ │ │
│ └─────────┼──────────────────┼─────────────────┼───────┘ │
│ └──────────────────┼─────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service Layer (业务逻辑) │ │
│ └──────────────────────┬──────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Repository Layer (数据访问) │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ │
│ │ │ RequirementDoc│ │ User │ │ History │ │ │
│ │ │ Repository │ │ Repository │ │Repository│ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └─────┬────┘ │ │
│ └─────────┼──────────────────┼─────────────────┼───────┘ │
└────────────┼──────────────────┼─────────────────┼───────────┘
└──────────────────┼─────────────────┘
↓
┌───────────────┐
│ MySQL 数据库 │
└───────────────┘
- 前端发起请求:根据页面需求,使用不同的 Fragment 组合查询
- GraphQL 解析:Spring for GraphQL 解析请求,确定需要哪些字段
- 按需执行:只执行对应字段的
@SchemaMapping方法 - 批量加载:DataLoader 收集所有待加载 ID,批量查询数据库
- 数据组装:将查询结果按 GraphQL Schema 组装返回
- 前端接收:前端只接收请求的字段,减少流量消耗
定义 Fragment:
# 基础字段片段
fragment RequirementDocBasicFields on RequirementDoc {
id
title
version
createdAt
}
# 带用户信息片段
fragment RequirementDocWithUser on RequirementDoc {
id
title
user {
username
avatar
}
}使用 Fragment:
# 列表页:只加载基础字段
query GetDocList {
requirementDocs(page: 0, size: 10) {
content {
...RequirementDocBasicFields
}
}
}
# 详情页:加载完整内容
query GetDocDetail($id: Long!) {
requirementDoc(id: $id) {
...RequirementDocWithUser
content
statistics {
wordCount
editCount
}
}
}后端实现:
@Controller
public class RequirementDocGraphQLController {
// 入口查询
@QueryMapping
public List<RequirementDoc> requirementDocs(@Argument Integer page, @Argument Integer size) {
return repository.findAll(PageRequest.of(page, size)).getContent();
}
// 字段解析:只有前端请求 user 字段时才执行
@SchemaMapping(typeName = "RequirementDoc", field = "user")
public CompletableFuture<User> user(RequirementDoc doc) {
// DataLoader 会自动批量加载
return CompletableFuture.supplyAsync(() ->
userRepository.findById(doc.getUserId()).orElse(null)
);
}
// 字段解析:只有前端请求 statistics 字段时才执行
@SchemaMapping(typeName = "RequirementDoc", field = "statistics")
public RequirementDocStatistics statistics(RequirementDoc doc) {
// 按需计算统计信息
return computeStatistics(doc);
}
}传统方式(N+1 问题):
查询 10 个文档:
SELECT * FROM requirement_docs LIMIT 10; -- 1 次
SELECT * FROM users WHERE id = 1; -- 10 次
SELECT * FROM users WHERE id = 2;
...
SELECT * FROM users WHERE id = 10;
总计:11 次查询
DataLoader 批量加载:
查询 10 个文档:
SELECT * FROM requirement_docs LIMIT 10; -- 1 次
SELECT * FROM users WHERE id IN (1,2,...,10); -- 1 次(批量)
总计:2 次查询
配置 DataLoader:
@Configuration
public class DataLoaderConfig {
@Bean
public BatchLoaderRegistry batchLoaderRegistry(UserRepository userRepository) {
BatchLoaderRegistry registry = new BatchLoaderRegistry();
// 用户批量加载器
registry.forTypePair(Long.class, User.class)
.registerMappedBatchLoader((userIds, batchEnvironment) -> {
// 批量查询所有用户
List<User> users = userRepository.findAllById(userIds);
// 转换为 Map 供 GraphQL 分发
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(User::getId, user -> user));
return Mono.just(userMap);
});
return registry;
}
}<!-- Spring for GraphQL -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<!-- GraphQL Extended Scalars -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-scalars</artifactId>
<version>21.0</version>
</dependency>spring:
graphql:
path: /graphql
graphiql:
enabled: true
path: /graphiql
schema:
locations: classpath:graphql/**/*.graphqls
cors:
allowed-origins:
- http://localhost:3000type Query {
requirementDocs(page: Int, size: Int): [RequirementDoc!]!
}
type RequirementDoc {
id: Long!
title: String!
content: String
user: User
statistics: RequirementDocStatistics
}@Controller
public class RequirementDocGraphQLController {
@QueryMapping
public List<RequirementDoc> requirementDocs(@Argument Integer page, @Argument Integer size) {
return repository.findAll(PageRequest.of(page, size)).getContent();
}
@SchemaMapping(typeName = "RequirementDoc", field = "user")
public User user(RequirementDoc doc) {
return userRepository.findById(doc.getUserId()).orElse(null);
}
}npm install axios// utils/graphqlClient.js
import axios from 'axios';
class GraphQLClient {
async execute({ query, variables }) {
const response = await axios.post('/graphql', {
query,
variables
});
return response.data;
}
}
export default new GraphQLClient();// graphql/fragments.js
export const BASIC_FIELDS = `
fragment BasicFields on RequirementDoc {
id
title
createdAt
}
`;<script setup>
import graphqlClient from '@/utils/graphqlClient';
import { BASIC_FIELDS } from '@/graphql/fragments';
const loadDocs = async () => {
const query = `
${BASIC_FIELDS}
query GetDocs {
requirementDocs {
...BasicFields
}
}
`;
const result = await graphqlClient.execute({ query });
return result.data.requirementDocs;
};
</script>需求:展示文档列表,只需要标题、版本、创建时间
Fragment 定义:
fragment RequirementDocBasicFields on RequirementDoc {
id
title
version
createdAt
}查询:
query GetRequirementDocsList($page: Int, $size: Int) {
requirementDocs(page: $page, size: $size) {
content {
...RequirementDocBasicFields
}
pageInfo {
totalElements
hasNext
}
}
}优势:
- 每条记录约 200 bytes
- 100 条记录仅需传输 ~20KB
- 不触发 content、user、statistics 等字段的加载
需求:展示文档列表 + 作者头像和名称
Fragment 定义:
fragment RequirementDocWithUser on RequirementDoc {
id
title
version
user {
username
avatar
}
}查询:
query GetRequirementDocsWithUser($page: Int, $size: Int) {
requirementDocs(page: $page, size: $size) {
content {
...RequirementDocWithUser
}
}
}后端处理:
@SchemaMapping(typeName = "RequirementDoc", field = "user")
public CompletableFuture<User> user(RequirementDoc doc) {
// DataLoader 会收集所有 userId,一次性批量查询
return CompletableFuture.supplyAsync(() ->
userRepository.findById(doc.getUserId()).orElse(null)
);
}优势:
- DataLoader 自动批量加载用户
- 10 个文档 = 1 次文档查询 + 1 次批量用户查询(共 2 次 SQL)
- 避免了传统的 N+1 查询问题
需求:展示完整文档内容、作者信息、统计数据
Fragment 定义:
fragment RequirementDocFullDetail on RequirementDoc {
id
title
content
version
createdAt
updatedAt
user {
username
email
avatar
}
statistics {
wordCount
editCount
lastEditedAt
published
}
}查询:
query GetRequirementDocDetail($id: Long!) {
requirementDoc(id: $id) {
...RequirementDocFullDetail
}
}后端处理:
@SchemaMapping(typeName = "RequirementDoc", field = "statistics")
public RequirementDocStatistics statistics(RequirementDoc doc) {
// 只在前端请求 statistics 时才计算
int wordCount = doc.getContent() != null ? doc.getContent().length() : 0;
int editCount = historyRepository.countByDocId(doc.getId());
return RequirementDocStatistics.builder()
.wordCount(wordCount)
.editCount(editCount)
.lastEditedAt(doc.getUpdatedAt())
.published(false)
.build();
}优势:
- 统计信息按需计算,列表页不会浪费计算资源
- 前端可灵活组合需要的字段
- 减少不必要的数据传输
场景:查询 100 个文档
| 方案 | 数据量 | 说明 |
|---|---|---|
| REST(完整对象) | ~500KB | 包含所有字段,无论是否需要 |
| GraphQL(基础字段) | ~20KB | 只传输 id、title、version |
| GraphQL(带用户) | ~35KB | 额外加载 user.username、avatar |
| GraphQL(完整内容) | ~500KB | 详情页加载完整内容 |
流量节省:列表页使用基础字段,节省 96% 流量
N+1 问题示例:
传统方式(查询 10 个文档 + 作者):
SELECT * FROM requirement_docs LIMIT 10; -- 1 次
SELECT * FROM users WHERE id = 1; -- 10 次
...
总计:11 次查询,耗时 ~110ms
DataLoader 优化:
DataLoader 方式:
SELECT * FROM requirement_docs LIMIT 10; -- 1 次
SELECT * FROM users WHERE id IN (1,2,...,10); -- 1 次
总计:2 次查询,耗时 ~20ms
性能提升:查询次数减少 82%,响应时间减少 82%
统计信息计算:
@SchemaMapping(typeName = "RequirementDoc", field = "statistics")
public RequirementDocStatistics statistics(RequirementDoc doc) {
// 只有前端请求时才执行
// 列表页不请求 statistics,节省计算资源
return computeStatistics(doc);
}优势:
- 列表页:不计算统计信息,节省 CPU
- 详情页:按需计算,提供完整数据
# 按用途命名
fragment RequirementDocBasicFields on RequirementDoc { ... }
fragment RequirementDocWithUser on RequirementDoc { ... }
fragment RequirementDocFullContent on RequirementDoc { ... }
# 按页面命名
fragment RequirementDocListItem on RequirementDoc { ... }
fragment RequirementDocDetailView on RequirementDoc { ... }
fragment RequirementDocAdminView on RequirementDoc { ... }❌ 不推荐:
query {
requirementDoc(id: 1) {
user {
requirementDocs {
user {
requirementDocs {
... # 循环嵌套
}
}
}
}
}
}✅ 推荐:
# 限制嵌套深度
query {
requirementDoc(id: 1) {
user {
username
avatar
}
}
}适用场景:
- 一对多关系(如文档 → 作者)
- 需要批量加载的场景
- 高频查询的关联数据
不适用场景:
- 一次性查询的单个对象
- 复杂的聚合计算
- 实时性要求极高的数据
@SchemaMapping(typeName = "RequirementDoc", field = "user")
public User user(RequirementDoc doc) {
try {
return userRepository.findById(doc.getUserId())
.orElseThrow(() -> new RuntimeException("User not found"));
} catch (Exception e) {
log.error("Failed to load user for doc {}", doc.getId(), e);
return null; // 返回 null,不影响其他字段
}
}权限控制:
@SchemaMapping(typeName = "RequirementDoc", field = "tokenUsage")
public TokenUsageInfo tokenUsage(RequirementDoc doc, @AuthenticationPrincipal UserPrincipal principal) {
// 只有管理员才能查看 Token 使用情况
if (!principal.hasRole("ADMIN")) {
throw new AccessDeniedException("Access denied");
}
return computeTokenUsage(doc);
}查询复杂度限制:
spring:
graphql:
schema:
introspection:
enabled: false # 生产环境禁用 Schema 探查- 按需加载:前端通过 Fragments 精确控制数据需求
- 避免 N+1:DataLoader 自动批量加载,性能优异
- 声明式编程:
@SchemaMapping清晰表达字段解析逻辑 - 灵活组合:同一个 API 满足多种场景
- 类型安全:Schema 定义提供完整类型检查
- ✅ 前端页面数据需求差异化大
- ✅ 存在大量关联数据查询
- ✅ 需要灵活组合字段
- ✅ 移动端需要减少流量消耗
- ✅ 多客户端(Web/Mobile/Desktop)共用 API
- 访问 GraphiQL 界面:
http://localhost:5000/graphiql - 测试查询示例:参考
vue-app/src/graphql/requirementDoc.fragments.js - 查看演示页面:访问
/graphql-demo路由 - 监控性能:使用 Spring Boot Actuator 查看 GraphQL 指标
📖 更多文档: