为 Spring Boot 项目添加数据库支持
在教程的这个部分, 你将会使用 JDBC 向你的项目添加并配置一个数据库. 在 JVM 应用程序中, 你要使用 JDBC 来操作数据库. 为了方便, Spring Framework 提供了 JdbcTemplate
类, 简化 JDBC 的使用, 并帮助避免常见的错误.
添加数据库支持
在使用 Spring Framework 的应用程序中, 通常的做法是在所谓的 服务(Service) 层实现数据库访问逻辑 – 这是实现业务逻辑的地方. 在 Spring 中, 你需要使用 @Service
注解来标注类, 表示类属于应用程序的服务层. 在这个应用程序中, 你将会创建 MessageService
类来实现这个目的.
在相同的包中, 创建 MessageService.kt
文件, 其中包含 MessageService
类, 如下:
- 构造器参数与依赖注入 – (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
类:
- @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 的字符串:
但是, 将 null
作为 id
值保存到数据库是不正确的: 你需要恰当的处理这样的情况.
更新你的 MessageService.kt
文件中的代码, 在将 message 保存到时数据库, 如果 id
为 null
, 生成新的值:
- Elvis 操作符 – ?:
代码
message.id ?: UUID.randomUUID().toString()
使用了 Elvis 操作符 (if-not-null-else 的缩写)?:
. 如果?:
左侧的表达式不是null
, Elvis 操作符会返回这个表达式的值; 否则, 它返回右侧表达式的值. 注意, 右侧表达式只有在左侧表达式为null
的情况下才会计算.
应用程序代码已经可以访问数据库了. 现在需要配置数据源.
配置数据库
在应用程序中配置数据库:
在
src/main/resources
目录中创建schema.sql
文件. 它将会保存数据库对象的定义:更新
src/main/resources/schema.sql
文件, 内容如下:-- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL );它创建
messages
表, 包含 2 个列:id
和text
. 表结构与Message
类一致.打开
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:
运行应用程序. 应用程序启动之后, 你可以执行 POST 请求来向数据库存储消息.
在项目的根文件夹中创建
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/执行所有的 POST 请求. 使用请求声明侧栏中的绿色 Run 图标. 这些请求会将消息写入到数据库:
执行 GET 请求, 并在 Run 工具窗口查看结果:
执行请求的其它方式
你也可以使用任何其它的 HTTP Client, 或 cURL 命令行工具. 比如, 在终端中运行以下命令, 得到同样的结果:
通过 id 获取 message
为应用程序增加新的功能, 通过 id 来获取单个的 message.
在
MessageService
类中, 添加新的函数findMessageById(id: String)
, 通过 id 来获取单个的 message:// MessageService.kt package 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 值 } }向
MessageController
类添加新的index(...)
函数, 参数是id
:// MessageController.kt package 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.- vararg 参数在参数列表中的位置
query()
函数接受 3 个参数:SQL 查询字符串, 它执行时需要一个参数
id
, 类型为字符串的参数RowMapper
实例, 由 Lambda 表达式实现
query()
函数的第 2 个参数声明为 不定数量参数 (vararg
). 在 Kotlin 中, 不定数量参数的位置并不要求是在参数列表的最后.- 接受者可为 null 的扩展函数
扩展函数可以使用可为 null 的接受者类型. 如果接受者为
null
, 那么this
也是null
. 因此在定义接受者可为 null 的扩展函数时, 建议在函数的 body 部之内执行this == null
检查.你也可以使用 null 值安全的调用操作符 (
?.
) 来进行 null 值检查, 就象上面的toResponseBody
函数那样:this?.let { ResponseEntity.ok(it) }- ResponseEntity
ResponseEntity
表示 HTTP 应答, 包含 Status Code, Header, 以及 Body. 它是一个通用的封装, 你可以用它向客户端发送自定义的 HTTP 应答, 对应答内容进行更好的控制.
下面是应用程序的完整代码:
运行应用程序
Spring 应用程序已经可以运行了:
再次运行应用程序.
打开
requests.http
文件, 添加新的 GET 请求:### 根据 id 得到 message GET http://localhost:8080/id执行 GET 请求, 从数据库得到所有的 message.
在 Run 工具窗口, 复制某个 message 的 id, 并添加到请求中, 例如:
### 根据 id 得到 message GET http://localhost:8080/f16c1d2e-08dc-455c-abfe-68440229b84f执行 GET 请求, 并在 Run 工具窗口中查看结果:
下一步
本教程的最后部分会向你演示, 如何使用更加流行的数据库操作方式 Spring Data.