为 Spring Boot 项目添加数据库支持
这是 **Spring Boot 和 Kotlin 入门** 教程的第三部分。在继续之前,请确保已完成以下步骤:
使用 Kotlin 创建 Spring Boot 项目
向 Spring Boot 项目添加数据类
为 Spring Boot 项目添加数据库支持
使用 Spring Data CrudRepository 进行数据库访问
在本教程的这一部分,你将使用 Java 数据库连接 (JDBC) 为你的项目添加和配置数据库。 在 JVM 应用程序中,你使用 JDBC 与数据库进行交互。 为方便起见,Spring Framework 提供了 JdbcTemplate
类,它简化了 JDBC 的使用并有助于避免常见错误。
添加数据库支持
在基于 Spring Framework 的应用程序中,通常的做法是在所谓的 服务 层实现数据库访问逻辑——业务逻辑就驻留在此处。 在 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 表达式和 SAM 转换
findMessages()
函数调用 JdbcTemplate
类的 query()
函数。query()
函数接受两个参数:一个作为 String 实例的 SQL 查询,以及一个将每行映射为一个对象的回调:
db.query("...", RowMapper { ... } )
RowMapper
接口只声明了一个方法,因此可以通过省略接口名称的 lambda 表达式来实现它。Kotlin 编译器知道 lambda 表达式需要转换成的接口,因为你将其用作函数调用的参数。这在 Kotlin 中被称为 SAM 转换:
db.query("...", { ... } )
经过 SAM 转换后,query 函数最终会有两个参数:第一个位置是 String,最后一个位置是 lambda 表达式。根据 Kotlin 约定,如果函数的最后一个参数是函数类型,那么作为相应参数传递的 lambda 表达式可以放在括号外面。这种语法也称为 尾随 lambda 表达式:
db.query("...") { ... }
未使用的 lambda 参数的下划线
对于具有多个参数的 lambda 表达式,你可以使用下划线 _
字符来替换你未使用的参数的名称。
因此,查询函数调用的最终语法如下:
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 请求体内容发送的 JSON 转换为对象,你需要为方法参数使用 @RequestBody
注解。由于应用程序的类路径中包含 Jackson 库,转换会自动发生。
ResponseEntity
ResponseEntity
表示整个 HTTP 响应:状态码、头部和响应体。
使用 created()
方法可以配置响应状态码 (201) 并设置 Location 头部,指示所创建资源的上下文路径。
更新 MessageService 类
Message
类的 id
被声明为一个可空字符串:
data class Message(val id: String?, val text: String)
但是,将 null
作为 id
值存储在数据库中是不正确的:你需要优雅地处理这种情况。
更新 MessageService.kt
文件中的代码,以便在将消息存储到数据库时,如果 id
为 null
则生成一个新值:
// 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() // Generate new id if it is null
db.update(
"insert into messages values ( ?, ? )",
id, message.text
)
return message.copy(id = id) // Return a copy of the message with the new id
}
}
Elvis 运算符 – ?:
代码 message.id ?: UUID.randomUUID().toString()
使用了 Elvis 运算符 (if-not-null-else 简写) ?:
。如果 ?:
左侧的表达式不是 null
,Elvis 运算符会返回它;否则,它会返回右侧的表达式。请注意,右侧的表达式仅在左侧表达式为 null
时才进行求值。
应用程序代码已准备好与数据库一起工作。现在需要配置数据源。
配置数据库
在应用程序中配置数据库:
在
src/main/resources
目录下创建schema.sql
文件。它将存储数据库对象定义:使用以下代码更新
src/main/resources/schema.sql
文件:sql-- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL );
它创建了一个名为
messages
的表,包含两列:id
和text
。表结构与Message
类的结构匹配。打开位于
src/main/resources
文件夹中的application.properties
文件,并添加以下应用程序属性:nonespring.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 请求向数据库添加消息
你应该使用 HTTP 客户端来处理之前创建的端点。在 IntelliJ IDEA 中,使用嵌入式 HTTP 客户端:
运行应用程序。一旦应用程序启动并运行,你就可以执行 POST 请求将消息存储到数据库中。
在项目根文件夹中创建
requests.http
文件并添加以下 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!" } ### Get all the messages GET http://localhost:8080/
执行所有 POST 请求。使用请求声明旁边的绿色 运行 图标。 这些请求将文本消息写入数据库:
执行 GET 请求并在 运行 工具窗口中查看结果:
执行请求的替代方式
你也可以使用任何其他 HTTP 客户端或 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 检索消息
扩展应用程序的功能,以便通过 ID 检索单个消息。
在
MessageService
类中,添加新函数findMessageById(id: String)
以通过 ID 检索单个消息: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<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() // Generate new id if it is null db.update( "insert into messages values ( ?, ? )", id, message.text ) return message.copy(id = id) // Return a copy of the message with the new id } }
- 运行需要参数的 SQL 查询字符串
id
,一个 String 类型的参数- 由 lambda 表达式实现的
RowMapper
实例
参数列表中的 vararg 参数位置
query()
函数接受三个参数:query()
函数的第二个参数被声明为 _可变参数_ (vararg
)。在 Kotlin 中,可变参数在参数列表中的位置不要求是最后一个。singleOrNull() 函数
singleOrNull()
函数返回一个单个元素,如果数组为空或有多个具有相同值的元素,则返回null
。DANGER
用于通过 ID 获取消息的
.query()
函数是 Spring Framework 提供的 Kotlin 扩展函数。它需要额外的导入import org.springframework.jdbc.core.query
,如上面的代码所示。在
MessageController
类中添加带有id
参数的新函数getMessage(...)
: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<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> = // If the message is null (not found), set response code to 404 this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() }
从上下文路径中检索值
消息
id
由 Spring Framework 从上下文路径中检索,因为你使用@GetMapping("/{id}")
注解了新函数。通过使用@PathVariable
注解函数参数,你告诉框架将检索到的值用作函数参数。新函数调用MessageService
以通过其 ID 检索单个消息。带可空接收者的扩展函数
扩展可以定义为可空接收者类型。如果接收者为
null
,那么this
也为null
。因此,在定义可空接收者类型的扩展时,建议在函数体内部执行this == null
检查。你也可以使用空安全调用运算符 (
?.
) 来执行空检查,如上面的toResponseEntity()
函数所示:kotlinthis?.let { ResponseEntity.ok(it) }
ResponseEntity
ResponseEntity
表示 HTTP 响应,包括状态码、头部和响应体。它是一个通用包装器,允许你向客户端发送自定义的 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 应用程序已准备好运行:
再次运行应用程序。
打开
requests.http
文件并添加新的 GET 请求:http### Get the message by its id GET http://localhost:8080/id
执行 GET 请求以从数据库中检索所有消息。
在 运行 工具窗口中复制一个 ID,并将其添加到请求中,如下所示:
http### Get the message by its id GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707
NOTE
请将你的消息 ID 替换为上面提到的 ID。
执行 GET 请求并在 运行 工具窗口中查看结果:
下一步
最后一步将向你展示如何使用 Spring Data 进行更流行的数据库连接。