GraphQL:GraphQL-Kotlin、查询与变更、订阅、数据加载器
说实话,REST API 用久了,你总会遇到一些让人头疼的问题。比如前端要展示一个用户信息,你得调三四个接口,自己拼数据。或者后端接口返回了一大堆字段,前端其实只需要其中两个。嗯,GraphQL 就是来解决这些问题的。
我个人习惯把 GraphQL 理解成「前端的 SQL」。前端想要什么数据,自己写查询语句,后端只返回你要的。不多不少,刚刚好。今天我们就聊聊在 Kotlin 生态里怎么玩转 GraphQL。
为什么是 GraphQL-Kotlin?
Kotlin 社区里 GraphQL 的库有好几个,但我用得最多的是 GraphQL-Kotlin。为什么?因为它跟 Spring Boot 集成得特别好,而且完全用 Kotlin 的特性来定义 Schema,不用写那些繁琐的 .graphqls 文件。
我记得第一次在项目里引入它时,最让我惊喜的是它支持用 Kotlin 的 data class 直接映射 GraphQL 类型。你想想看,少写多少模板代码啊。
- 纯 Kotlin 实现,没有 Java 遗留包袱
- 与 Spring Boot / Ktor 深度集成
- 支持协程,天生异步
- 代码即文档,类型安全
快速搭建一个 GraphQL 服务
先加依赖。我用的是 Gradle Kotlin DSL:
// build.gradle.kts
implementation("com.expediagroup:graphql-kotlin-spring-server:7.0.0")
implementation("com.expediagroup:graphql-kotlin-ktor-server:7.0.0") // 如果用 Ktor
然后定义一个简单的查询。说白了就是写个普通的 Kotlin 类:
class Query {
fun hello(): String = "Hello, GraphQL!"
fun user(id: String): User = User(id, "张三", 28)
}
data class User(
val id: String,
val name: String,
val age: Int
)
嗯,就这么简单。GraphQL-Kotlin 会自动把 Kotlin 的 data class 映射成 GraphQL 的 type。你不需要写任何 Schema 文件。
配置一下 Spring Boot 的入口:
@SpringBootApplication
@GraphQLSchema
class Application
fun main() {
runApplication<Application>()
}
启动后访问 /graphql 端点,你就可以用 GraphQL 的 Playground 测试了。
查询与变更:CRUD 的 GraphQL 版
查询(Query)就是读数据,变更(Mutation)就是写数据。这个区分跟 REST 的 GET 和 POST 有点像,但更灵活。
我在项目中遇到过这样一个场景:前端需要同时获取用户信息和用户的订单列表。用 REST 你得调两个接口,用 GraphQL 一次搞定:
// 定义查询
class Query {
fun user(id: String): User = userRepository.findById(id)
fun users(): List<User> = userRepository.findAll()
}
// 定义变更
class Mutation {
fun createUser(name: String, age: Int): User {
val user = User(UUID.randomUUID().toString(), name, age)
return userRepository.save(user)
}
fun updateUser(id: String, name: String?, age: Int?): User {
val user = userRepository.findById(id)
return user.copy(
name = name ?: user.name,
age = age ?: user.age
).also { userRepository.save(it) }
}
}
前端发起的查询长这样:
query {
user(id: "123") {
id
name
age
orders {
id
total
status
}
}
}
看到了吗?前端只取它需要的字段,后端也只返回这些字段。带宽省了,性能也上去了。
订阅:实时推送的优雅方案
订阅(Subscription)是 GraphQL 里我最喜欢的功能之一。它让后端可以主动推送数据给前端,适合做聊天、通知、实时数据看板这类场景。
实现起来也不复杂:
class Subscription {
fun userUpdates(): Flow<User> = userUpdateFlow
}
// 在变更里触发更新
class Mutation {
fun updateUser(id: String, name: String): User {
val updated = userRepository.update(id, name)
userUpdateFlow.emit(updated) // 推送更新
return updated
}
}
前端订阅的写法:
subscription {
userUpdates {
id
name
age
}
}
这里用到了 Kotlin 的 Flow,天然支持背压和取消。我曾经在做一个实时监控面板时,用订阅推送服务器指标数据,效果非常好。前端不用轮询,后端有变化才推送,资源消耗降了一大截。
数据加载器:解决 N+1 查询问题
这是 GraphQL 里最容易踩坑的地方。你想想看,如果查询一个用户列表,每个用户又关联了订单,默认情况下 GraphQL 会为每个用户单独查一次订单表。100 个用户就是 101 次查询(1 次查用户 + 100 次查订单)。这就是经典的 N+1 问题。
我曾经在一个项目里上线后才发现这个问题,数据库直接被查崩了。后来用 DataLoader 才解决。
GraphQL-Kotlin 集成了 DataLoader,用法很简单:
class UserDataLoader : DataLoader<String, User>({ keys ->
// 批量查询,一次 SQL 搞定
val users = userRepository.findByIds(keys)
keys.map { key -> users.find { it.id == key } }
})
// 在查询中使用
class Query {
fun users(): List<User> = userRepository.findAll()
}
// 在 User 类型中关联订单
class User(
val id: String,
val name: String,
val age: Int
) {
// 使用 DataLoader 延迟加载订单
fun orders(dataLoader: DataLoader<String, List<Order>>): CompletableFuture<List<Order>> {
return dataLoader.load(id)
}
}
这样,不管查询多少个用户,订单查询只会执行一次。性能提升是数量级的。
@GraphQLContext 使用,否则拿不到 DataLoader 实例。我曾经在这里卡了半天,最后发现是上下文传递的问题。
知识体系总览
下面这张图概括了 GraphQL-Kotlin 的核心模块和它们之间的关系:
实战中的一些建议
做了几个 GraphQL 项目后,我总结了几条经验:
- 不要滥用嵌套:GraphQL 允许无限嵌套,但深度超过 5 层就要小心了。我见过有人嵌套了 10 层,一个查询跑了 30 秒。建议用
maxDepth限制一下。 - 善用片段:前端如果多个地方用到相同的字段组合,用 fragment 复用。后端不用管,前端自己组织。
- 监控查询性能:GraphQL 的查询是动态的,你很难预判前端会怎么查。建议加上查询耗时分析和慢查询日志。
- 接口版本管理:GraphQL 不需要版本号,但字段废弃(deprecated)一定要标注。我习惯在 Schema 里加
@deprecated注解,给前端一个缓冲期。
MutationResult 包装类,包含 success、message、data 三个字段。这样前端处理错误逻辑统一,不用每个接口单独写 try-catch。
好了,关于 GraphQL-Kotlin 的核心内容就这些。查询、变更、订阅、数据加载器,这四个东西搞明白了,大部分场景都能应付。剩下的就是在项目里多踩坑、多积累了。
公众号:蓝海资料掘金营,微信deep3321