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,天然支持背压和取消。我曾经在做一个实时监控面板时,用订阅推送服务器指标数据,效果非常好。前端不用轮询,后端有变化才推送,资源消耗降了一大截。

注意: 订阅默认走 WebSocket。如果你的部署环境不支持 WebSocket(比如某些负载均衡器),需要额外配置 STOMP 或 SSE 作为传输层。

数据加载器:解决 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)
    }
}

这样,不管查询多少个用户,订单查询只会执行一次。性能提升是数量级的。

避坑指南: DataLoader 一定要配合 @GraphQLContext 使用,否则拿不到 DataLoader 实例。我曾经在这里卡了半天,最后发现是上下文传递的问题。

知识体系总览

下面这张图概括了 GraphQL-Kotlin 的核心模块和它们之间的关系:

GraphQL-Kotlin 核心架构 客户端层 Web / Mobile / CLI 发送 GraphQL 查询、变更、订阅请求 传输层 HTTP (查询/变更) | WebSocket (订阅) GraphQL 引擎 Schema 解析 → 查询验证 → 执行器 → 数据加载器 DataLoader 批量加载,解决 N+1 问题 数据源层 数据库 (JPA/Exposed) | 外部 API | 缓存 (Redis) | 消息队列

实战中的一些建议

做了几个 GraphQL 项目后,我总结了几条经验:

  • 不要滥用嵌套:GraphQL 允许无限嵌套,但深度超过 5 层就要小心了。我见过有人嵌套了 10 层,一个查询跑了 30 秒。建议用 maxDepth 限制一下。
  • 善用片段:前端如果多个地方用到相同的字段组合,用 fragment 复用。后端不用管,前端自己组织。
  • 监控查询性能:GraphQL 的查询是动态的,你很难预判前端会怎么查。建议加上查询耗时分析和慢查询日志。
  • 接口版本管理:GraphQL 不需要版本号,但字段废弃(deprecated)一定要标注。我习惯在 Schema 里加 @deprecated 注解,给前端一个缓冲期。
个人习惯: 我会在 Mutation 里统一返回一个 MutationResult 包装类,包含 success、message、data 三个字段。这样前端处理错误逻辑统一,不用每个接口单独写 try-catch。

好了,关于 GraphQL-Kotlin 的核心内容就这些。查询、变更、订阅、数据加载器,这四个东西搞明白了,大部分场景都能应付。剩下的就是在项目里多踩坑、多积累了。


公众号:蓝海资料掘金营,微信deep3321