# 为 Spring Boot 项目添加数据库支持 在教程的这个部分, 你将会使用 Java 数据库连接 (Java Database Connectivity, JDBC) 向你的项目添加并配置一个数据库. 在 JVM 应用程序中, 你要使用 JDBC 来操作数据库. 为了方便, Spring Framework 提供了 `JdbcTemplate` 类, 简化 JDBC 的使用, 并帮助避免常见的错误. ## 添加数据库支持 在使用 Spring Framework 的应用程序中, 通常的做法是在所谓的 服务(Service) 层实现数据库访问逻辑 – 这是实现业务逻辑的地方. 在 Spring 中, 你需要使用 `@Service` 注解来标注类, 表示类属于应用程序的服务层. 在这个应用程序中, 你将会创建 `MessageService` 类来实现这个目的. 在相同的包中, 创建 `MessageService.kt` 文件, 其中包含 `MessageService` 类, 如下: ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List = 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 中的类有一个主构造器. 还可以有一个或多个 [次级构造器](classes.html#secondary-constructors). 主构造器 是类头部的一部分, 位于类名称以及可选的类型参数之后. 在我们的例子中, 构造器是 `(val db: JdbcTemplate)`. : : : : `val db: JdbcTemplate` 是构造器的参数: : : : : : ```KOTLIN : @Service : class MessageService(private val db: JdbcTemplate) : ``` 尾缀 Lambda 表达式(Trailing Lambda) 与 SAM 转换 : `findMessages()` 函数调用 `JdbcTemplate` 类的 `query()` 函数. `query()` 函数接受 2 个参数: 一个 SQL 查询, 类型为字符串, 以及一个回调, 将每一行查询结果转换为对象: : : : : : ```SQL : db.query("...", RowMapper { ... } ) : ``` : : : : : `RowMapper` 接口只声明了一个方法, 因此可以使用 Lambda 表达式来实现它, 省略接口名称. Kotlin 编译器知道表达式需要转换成的接口, 因为你将它用作函数调用的一个参数. 这个功能称为 [Kotlin 中的SAM 转换](java-interop.html#sam-conversions): : : : : : ```SQL : db.query("...", { ... } ) : ``` : : : : : 在 SAM 转换之后, query 函数得到 2 个参数: 首先是一个 String, 后面是一个 Lambda 表达式. 根据 Kotlin 的习惯, 如果一个函数的最后一个参数是一个函数, 那么传递给这个参数的 Lambda 表达式可以放在括号之外. 这样的语法称为 [尾缀 Lambda 表达式(Trailing Lambda)](lambdas.html#passing-trailing-lambdas): : : : : : ```SQL : db.query("...") { ... } : ``` 对未使用的 Lambda 表达式参数使用下划线 : 对于带有多个参数的 Lambda 表达式, 你可以使用下划线 `_` 符号来代替你不需要使用的参数的名称. : : : : 因此, query 函数调用的最终语法如下: : : : : : ```KOTLIN : db.query("select * from messages") { response, _ -> : Message(response.getString("id"), response.getString("text")) : } : ``` ## 更新 MessageController 类 更新 `MessageController.kt`, 使用新的 `MessageService` 类: ```KOTLIN // 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 { 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 的字符串: ```KOTLIN data class Message(val id: String?, val text: String) ``` 但是, 将 `null` 作为 `id` 值保存到数据库是不正确的: 你需要恰当的处理这样的情况. 更新你的 `MessageService.kt` 文件中的代码, 在将 message 保存到时数据库, 如果 `id` 为 `null`, 生成新的值: ```KOTLIN // MessageService.kt package com.example.demo import org.springframework.stereotype.Service import org.springframework.jdbc.core.JdbcTemplate import java.util.UUID @Service class MessageService(private val db: JdbcTemplate) { fun findMessages(): List = 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-safety.html#elvis-operator). 如果 `?:` 左侧的表达式不是 `null`, Elvis 操作符会返回这个表达式的值; 否则, 它返回右侧表达式的值. 注意, 右侧表达式只有在左侧表达式为 `null` 的情况下才会计算. 应用程序代码已经可以访问数据库了. 现在需要配置数据源. ## 配置数据库 在应用程序中配置数据库: 1. 在 `src/main/resources` 目录中创建 `schema.sql` 文件. 它将会保存数据库对象的定义: ![创建数据库 Schema](images/create-database-schema.png) 2. 更新 `src/main/resources/schema.sql` 文件, 内容如下: ```SQL -- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL ); ``` 它创建 `messages` 表, 包含 2 个列: `id` 和 `text`. 表结构与 `Message` 类一致. 3. 打开 `src/main/resources` 文件夹内的 `application.properties` 文件, 添加以下应用程序属性: ```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 文档](https://docs.spring.io/spring-boot/appendix/application-properties/index.html). ## 通过 HTTP 请求, 向数据库添加 message 你应该使用一个 HTTP 客户端来访问前面创建的 Endpoint. 在 IntelliJ IDEA 中, 请使用内嵌的 HTTP Client: 1. 运行应用程序. 应用程序启动之后, 你可以执行 POST 请求来向数据库存储消息. 2. 在项目的根文件夹中创建 `requests.http` 文件, 并添加以下 HTTP 请求: ```HTTP_REQUEST_FULL ### 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 请求](images/execute-post-requests.png) 4. 执行 GET 请求, 并在 Run 工具窗口查看结果: ![执行 GET 请求](images/execute-get-requests.png) ### 执行请求的其它方式 你也可以使用任何其它的 HTTP Client, 或 cURL 命令行工具. 比如, 在终端中运行以下命令, 得到同样的结果: ```BASH 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: ```KOTLIN // 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 = 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()](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/single-or-null.html) 函数返回单个元素, 如果数组为空, 或存在相同值的多个元素, 则返回 `null`. > **Warning:** > 通过 id 来获取 message 的 `.query()` 函数是由 Spring Framework 提供的一个 [Kotlin 扩展函数](extensions.html#extension-functions), 如上面的代码所示, 它需要一个额外的 `import org.springframework.jdbc.core.query` 语句. 2. 向 `MessageController` 类添加新的 `index(...)` 函数, 参数是 `id`: ```KOTLIN // 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 { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } @GetMapping("/{id}") fun getMessage(@PathVariable id: String): ResponseEntity = service.findMessageById(id).toResponseEntity() private fun Message?.toResponseEntity(): ResponseEntity = // 如果 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()` 函数那样: : : : : : ```KOTLIN : this?.let { ResponseEntity.ok(it) } : ``` ResponseEntity : `ResponseEntity` 表示 HTTP 应答, 包含 Status Code, Header, 以及 Body. 它是一个通用的封装, 你可以用它向客户端发送自定义的 HTTP 应答, 对应答内容进行更好的控制. 下面是应用程序的完整代码: ```KOTLIN // DemoApplication.kt package com.example.demo import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array) { runApplication(*args) } ``` ```KOTLIN // Message.kt package com.example.demo data class Message(val id: String?, val text: String) ``` ```KOTLIN // 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 = 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) } } ``` ```KOTLIN // 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 { val savedMessage = service.save(message) return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage) } @GetMapping("/{id}") fun getMessage(@PathVariable id: String): ResponseEntity = service.findMessageById(id).toResponseEntity() private fun Message?.toResponseEntity(): ResponseEntity = this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() } ``` ## 运行应用程序 Spring 应用程序已经可以运行了: 1. 再次运行应用程序. 2. 打开 `requests.http` 文件, 添加新的 GET 请求: ```HTTP_REQUEST_FULL ### 根据 id 得到 message GET http://localhost:8080/id ``` 3. 执行 GET 请求, 从数据库得到所有的 message. 4. 在 Run 工具窗口, 复制某个 message 的 id, 并添加到请求中, 例如: ```HTTP_REQUEST_FULL ### 根据 id 得到 message GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707 ``` > **Note:** > 请使用你的 message 的真实 id, 不要使用上面例子中的值. 5. 执行 GET 请求, 并在 Run 工具窗口中查看结果: ![根据 id 得到 message](images/retrieve-message-by-its-id.png) ## 下一步 本教程的最后部分会向你演示, 如何使用更加流行的数据库操作方式 Spring Data. * [上一步](jvm-spring-boot-add-data-class.html) * [下一步](jvm-spring-boot-using-crudrepository.html)