引言:REST已死,GraphQL万岁?(剧透:并没有)
\n如果你正在使用Kotlin开发后端,那么你一定听说过这场圣战:GraphQL vs REST。擂台的一角是经典老将REST,每个初级开发者都熟悉它。另一角是时髦的GraphQL,它承诺“给我想要的东西,但只需一次请求”。
\n我是一名有Kotlin经验的后端开发者,在过去几年里,我将三个项目从REST迁移到GraphQL,然后又迁移回来。剧透:每种工具都有其适用的场景。本文不会盲目追捧。相反,我们将通过真实的代码示例来分析,何时Kotlin上的REST更胜一筹,何时GraphQL是你的救星。
\n让我们开始Kotlin中GraphQL与REST的对比,不掺杂水分和营销。
\n\n1. 核心概念速览:各自是什么
\n在深入代码之前,我们先统一术语。REST API是一种架构风格,其中每个资源(用户、订单、商品)都有独立的URL和HTTP方法(GET、POST、PUT、DELETE)。Kotlin开发者通常使用Spring Boot或Ktor来构建REST服务。
\nGraphQL是一种查询语言,只有一个端点(通常是/graphql),客户端自行决定需要哪些字段。在Kotlin中,流行的库有KGraphQL(轻量级)和Spring for GraphQL(适合Spring爱好者)。
关键区别:REST是“服务器决定返回什么”,GraphQL是“客户端只请求所需内容”。
\n\n2. Kotlin REST API示例(Ktor)
\n从经典开始。假设我们有一个电商网站,需要获取用户信息及其最近的订单。
\n以下是使用Ktor(Kotlin轻量级框架)的典型REST控制器:
\n\n// REST端点:GET /api/users/{id}\nfun Application.userModule() {\n routing {\n get("/api/users/{id}") {\n val userId = call.parameters["id"]?.toIntOrNull() ?: return@get call.respondText("Bad Request", status = HttpStatusCode.BadRequest)\n val user = userService.getUserById(userId) ?: return@get call.respondText("Not Found", status = HttpStatusCode.NotFound)\n call.respond(user)\n }\n }\n}\n\n// 响应的数据类\n@Serializable\ndata class UserResponse(\n val id: Int,\n val name: String,\n val email: String,\n val lastOrders: List<Order>\n)\n\n问题:如果客户端(移动应用)只需要用户的email,服务器仍然会返回完整的UserResponse对象,包括订单信息。这就是过度获取(over-fetching)——传输了多余的数据。在弱移动网络环境下,这很痛苦。
解决方案之一是创建多个细粒度端点:GET /api/users/{id}/email。但这会导致请求数量增加(欠获取(under-fetching))。
3. Kotlin GraphQL示例(Spring for GraphQL)
\n现在,同样的场景,但使用GraphQL。以下是模式定义(schema.graphqls):
\n\ntype Query {\n user(id: ID!): User\n}\n\ntype User {\n id: ID!\n name: String\n email: String\n lastOrders: [Order]\n}\n\ntype Order {\n id: ID!\n total: Float\n date: String\n}\n\n这是使用Spring for GraphQL(注解方式)的Kotlin代码:
\n\n@Controller\nclass UserController(private val userService: UserService) {\n\n @QueryMapping\n fun user(@Argument id: Int): User {\n return userService.getUserById(id) ?: throw UserNotFoundException("User not found")\n }\n}\n\n// 数据类,无多余字段\ndata class User(val id: Int, val name: String, val email: String, val lastOrders: List<Order>)\n\n现在,客户端可以只请求所需内容:
\n\nquery {\n user(id: 42) {\n email\n }\n}