Kotlin 语言参考文档 中文版 Help

为 Spring Boot 项目添加数据库支持

在教程的这个部分, 你将会使用 Java 数据库连接 (Java Database Connectivity, JDBC) 向你的项目添加并配置一个数据库. 在 JVM 应用程序中, 你要使用 JDBC 来操作数据库. 为了方便, Spring Framework 提供了 JdbcTemplate 类, 简化 JDBC 的使用, 并帮助避免常见的错误.

添加数据库支持

在使用 Spring Framework 的应用程序中, 通常的做法是在所谓的 服务(Service) 层实现数据库访问逻辑 – 这是实现业务逻辑的地方. 在 Spring 中, 你需要使用 @Service 注解来标注类, 表示类属于应用程序的服务层. 在这个应用程序中, 你将会创建 MessageService 类来实现这个目的.

在相同的包中, 创建 MessageService.kt 文件, 其中包含 MessageService 类, 如下:

// MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import java.util.* @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message): Message { db.update( "insert into messages values ( ?, ? )", message.id, message.text ) return message } }
构造器参数与依赖注入 – (private val db: JdbcTemplate)

Kotlin 中的类有一个主构造器. 还可以有一个或多个 次级构造器. 主构造器 是类头部的一部分, 位于类名称以及可选的类型参数之后. 在我们的例子中, 构造器是 (val db: JdbcTemplate).

val db: JdbcTemplate 是构造器的参数:

@Service class MessageService(private val db: JdbcTemplate)
尾缀 Lambda 表达式(Trailing Lambda) 与 SAM 转换

findMessages() 函数调用 JdbcTemplate 类的 query() 函数. query() 函数接受 2 个参数: 一个 SQL 查询, 类型为字符串, 以及一个回调, 将每一行查询结果转换为对象:

db.query("...", RowMapper { ... } )

RowMapper 接口只声明了一个方法, 因此可以使用 Lambda 表达式来实现它, 省略接口名称. Kotlin 编译器知道表达式需要转换成的接口, 因为你将它用作函数调用的一个参数. 这个功能称为 Kotlin 中的SAM 转换:

db.query("...", { ... } )

在 SAM 转换之后, query 函数得到 2 个参数: 首先是一个 String, 后面是一个 Lambda 表达式. 根据 Kotlin 的习惯, 如果一个函数的最后一个参数是一个函数, 那么传递给这个参数的 Lambda 表达式可以放在括号之外. 这样的语法称为 尾缀 Lambda 表达式(Trailing Lambda):

db.query("...") { ... }
对未使用的 Lambda 表达式参数使用下划线

对于带有多个参数的 Lambda 表达式, 你可以使用下划线 _ 符号来代替你不需要使用的参数的名称.

因此, query 函数调用的最终语法如下:

db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) }

更新 MessageController 类

更新 MessageController.kt, 使用新的 MessageService 类:

// MessageController.kt package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController @RequestMapping("/") class MessageController(private val service: MessageService) { @GetMapping fun listMessages() = service.findMessages() @PostMapping fun post(@RequestBody message: Message): ResponseEntity<Message> { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } }
@PostMapping 注解

负责处理 HTTP POST 请求的方法需要标注 @PostMapping 注解. 为了将 HTTP 请求 Body 部的 JSON 内容转换为对象, 你需要对方法参数使用 @RequestBody 注解. 由于 Jackson 库存在于应用程序的类路径中, 这个转换能够自动完成.

ResponseEntity

ResponseEntity 表示完整的 HTTP 应答: Status Code, Header, 以及 Body.

使用 created() 方法, 你可以配置应答的 Status Code (201), 并设置 "Location" Header, 表示新创建的资源的上下文路径(context path).

更新 MessageService 类

Message 类的 id 声明为可为 null 的字符串:

data class Message(val id: String?, val text: String)

但是, 将 null 作为 id 值保存到数据库是不正确的: 你需要恰当的处理这样的情况.

更新你的 MessageService.kt 文件中的代码, 在将 message 保存到时数据库, 如果 idnull, 生成新的值:

// MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.query import java.util.UUID @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun save(message: Message): Message { val id = message.id ?: UUID.randomUUID().toString() // 如果 id 为 null, 生成新的 id 值 db.update( "insert into messages values ( ?, ? )", id, message.text ) return message.copy(id = id) // 返回 message 的 copy, 使用新的 id 值 } }
Elvis 操作符 – ?:

代码 message.id ?: UUID.randomUUID().toString() 使用了 Elvis 操作符 (if-not-null-else 的缩写) ?:. 如果 ?: 左侧的表达式不是 null, Elvis 操作符会返回这个表达式的值; 否则, 它返回右侧表达式的值. 注意, 右侧表达式只有在左侧表达式为 null 的情况下才会计算.

应用程序代码已经可以访问数据库了. 现在需要配置数据源.

配置数据库

在应用程序中配置数据库:

  1. src/main/resources 目录中创建 schema.sql 文件. 它将会保存数据库对象的定义:

    创建数据库 Schema
  2. 更新 src/main/resources/schema.sql 文件, 内容如下:

    -- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL );

    它创建 messages 表, 包含 2 个列: idtext. 表结构与 Message 类一致.

  3. 打开 src/main/resources 文件夹内的 application.properties 文件, 添加以下应用程序属性:

    spring.application.name=demo spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:file:./data/testdb spring.datasource.username=name spring.datasource.password=password spring.sql.init.schema-locations=classpath:schema.sql spring.sql.init.mode=always

    这些设置会为 Spring Boot 应用程序启用数据库. 关于完整的应用程序属性列表, 请参见 Spring 文档.

通过 HTTP 请求, 向数据库添加 message

你应该使用一个 HTTP 客户端来访问前面创建的 Endpoint. 在 IntelliJ IDEA 中, 请使用内嵌的 HTTP Client:

  1. 运行应用程序. 应用程序启动之后, 你可以执行 POST 请求来向数据库存储消息.

  2. 在项目的根文件夹中创建 requests.http 文件, 并添加以下 HTTP 请求:

    ### Post "Hello!" POST http://localhost:8080/ Content-Type: application/json { "text": "Hello!" } ### Post "Bonjour!" POST http://localhost:8080/ Content-Type: application/json { "text": "Bonjour!" } ### Post "Privet!" POST http://localhost:8080/ Content-Type: application/json { "text": "Privet!" } ### 得到所有的 message GET http://localhost:8080/
  3. 执行所有的 POST 请求. 使用请求声明侧栏中的绿色 Run 图标. 这些请求会将消息写入到数据库:

    执行 POST 请求
  4. 执行 GET 请求, 并在 Run 工具窗口查看结果:

    执行 GET 请求

执行请求的其它方式

你也可以使用任何其它的 HTTP Client, 或 cURL 命令行工具. 比如, 在终端中运行以下命令, 得到同样的结果:

curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Hello!\" }" curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Bonjour!\" }" curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Privet!\" }" curl -X GET --location "http://localhost:8080"

通过 id 获取 message

为应用程序增加新的功能, 通过 id 来获取单个的 message.

  1. MessageService 类中, 添加新的函数 findMessageById(id: String), 通过 id 来获取单个的 message:

    // MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.query import java.util.* @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun findMessageById(id: String): Message? = db.query("select * from messages where id = ?", id) { response, _ -> Message(response.getString("id"), response.getString("text")) }.singleOrNull() fun save(message: Message): Message { val id = message.id ?: UUID.randomUUID().toString() // 如果 id 为 null, 生成新的 id 值 db.update( "insert into messages values ( ?, ? )", id, message.text ) return message.copy(id = id) // 返回 message 的 copy, 使用新的 id 值 } }
    vararg 参数在参数列表中的位置

    query() 函数接受 3 个参数:

    • SQL 查询字符串, 它执行时需要一个参数

    • id, 类型为字符串的参数

    • RowMapper 实例, 由 Lambda 表达式实现

    query() 函数的第 2 个参数声明为 不定数量参数 (vararg). 在 Kotlin 中, 不定数量参数的位置并不要求是在参数列表的最后.

    singleOrNull() 函数

    singleOrNull() 函数返回单个元素, 如果数组为空, 或存在相同值的多个元素, 则返回 null.

  2. MessageController 类添加新的 index(...) 函数, 参数是 id:

    // MessageController.kt package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController @RequestMapping("/") class MessageController(private val service: MessageService) { @GetMapping fun listMessages() = ResponseEntity.ok(service.findMessages()) @PostMapping fun post(@RequestBody message: Message): ResponseEntity<Message> { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } @GetMapping("/{id}") fun getMessage(@PathVariable id: String): ResponseEntity<Message> = service.findMessageById(id).toResponseEntity() private fun Message?.toResponseEntity(): ResponseEntity<Message> = // 如果 message 为 null (未找到), 将应答的 Status Code 设置为 404 this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() }
    从 context 路径得到值

    Spring Framework 会从 context 路径得到 message 的 id 值, 因为你对新函数标注了 @GetMapping("/{id}") 注解. 通过对函数参数标注 @PathVariable 注解, 你告诉 Spring Framework 使用得到的值作为函数参数. 新函数会调用 MessageService 来通过 id 取得单个 message.

    接受者可为 null 的扩展函数

    扩展函数可以使用可为 null 的接受者类型. 如果接受者为 null, 那么 this 也是 null. 因此在定义接受者可为 null 的扩展函数时, 建议在函数的 body 部之内执行 this == null 检查.

    你也可以使用 null 值安全的调用操作符 (?.) 来进行 null 值检查, 就象上面的 toResponseEntity() 函数那样:

    this?.let { ResponseEntity.ok(it) }
    ResponseEntity

    ResponseEntity 表示 HTTP 应答, 包含 Status Code, Header, 以及 Body. 它是一个通用的封装, 你可以用它向客户端发送自定义的 HTTP 应答, 对应答内容进行更好的控制.

下面是应用程序的完整代码:

// DemoApplication.kt package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array<String>) { runApplication<DemoApplication>(*args) }
// Message.kt package com.example.demo data class Message(val id: String?, val text: String)
// MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import org.springframework.jdbc.core.query import java.util.* @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List<Message> = db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) } fun findMessageById(id: String): Message? = db.query("select * from messages where id = ?", id) { response, _ -> Message(response.getString("id"), response.getString("text")) }.singleOrNull() fun save(message: Message): Message { val id = message.id ?: UUID.randomUUID().toString() db.update( "insert into messages values ( ?, ? )", id, message.text ) return message.copy(id = id) } }
// MessageController.kt package com.example.demo import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.net.URI @RestController @RequestMapping("/") class MessageController(private val service: MessageService) { @GetMapping fun listMessages() = ResponseEntity.ok(service.findMessages()) @PostMapping fun post(@RequestBody message: Message): ResponseEntity<Message> { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } @GetMapping("/{id}") fun getMessage(@PathVariable id: String): ResponseEntity<Message> = service.findMessageById(id).toResponseEntity() private fun Message?.toResponseEntity(): ResponseEntity<Message> = this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() }

运行应用程序

Spring 应用程序已经可以运行了:

  1. 再次运行应用程序.

  2. 打开 requests.http 文件, 添加新的 GET 请求:

    ### 根据 id 得到 message GET http://localhost:8080/id
  3. 执行 GET 请求, 从数据库得到所有的 message.

  4. Run 工具窗口, 复制某个 message 的 id, 并添加到请求中, 例如:

    ### 根据 id 得到 message GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707

  5. 执行 GET 请求, 并在 Run 工具窗口中查看结果:

    根据 id 得到 message

下一步

本教程的最后部分会向你演示, 如何使用更加流行的数据库操作方式 Spring Data.

阅读下一章

2025/08/04