Skip to content

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

这是《Spring Boot 和 Kotlin 入门》教程的第三部分。在继续之前,请确保您已完成以下步骤:


First step 使用 Kotlin 创建 Spring Boot 项目
Second step 向 Spring Boot 项目添加数据类
Third step 为 Spring Boot 项目添加数据库支持
Fourth step 使用 Spring Data CrudRepository 进行数据库访问

本教程的这一部分中,您将使用 Java 数据库连接 (JDBC) 为项目添加和配置数据库。 在 JVM 应用程序中,您使用 JDBC 与数据库交互。 为了方便,Spring Framework 提供了 JdbcTemplate 类,该类简化了 JDBC 的使用并有助于避免常见错误。

添加数据库支持

在基于 Spring Framework 的应用程序中,常见的做法是在所谓的 服务 层中实现数据库访问逻辑 —— 这是业务逻辑所在的地方。 在 Spring 中,您应该使用 @Service 注解标记类,以表示该类属于应用程序的服务层。 在此应用程序中,您将为此目的创建 MessageService 类。

在同一包中,创建 MessageService.kt 文件和 MessageService 类,如下所示:

kotlin
// 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 是构造函数的实参:

kotlin
尾随 lambda 表达式与 SAM 转换

findMessages() 函数调用 JdbcTemplate 类的 query() 函数。query() 函数接受两个实参:一个作为 String 实例的 SQL 查询,以及一个将每行映射为一个对象的回调:

sql

RowMapper 接口只声明了一个方法,因此可以通过省略接口名称,用 lambda 表达式实现它。Kotlin 编译器知道 lambda 表达式需要转换成的接口,因为您将其用作函数调用的形参。这在 Kotlin 中被称为 SAM 转换

sql

SAM 转换后,query 函数最终得到两个实参:第一个位置的 String 和最后一个位置的 lambda 表达式。根据 Kotlin 约定,如果函数的最后一个形参是函数,则作为相应实参传递的 lambda 表达式可以放在圆括号之外。这种语法也被称为 尾随 lambda 表达式

sql
未使用的 lambda 实参的下划线

对于带有多个形参的 lambda 表达式,您可以使用下划线 _ 字符来替换您未使用的形参名称。

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

kotlin

更新 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<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 被声明为可空 String:

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

然而,将 null 作为 id 值存储在数据库中是不正确的:您需要优雅地处理这种情况。

更新 MessageService.kt 文件的代码,以便在将消息存储到数据库时,当 idnull 时生成一个新值:

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.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 操作符(非空即取简写) ?:。如果 ?: 左侧的表达式不为 null,Elvis 操作符会返回它;否则,它会返回右侧的表达式。请注意,右侧的表达式仅在左侧为 null 时才会被求值。

应用程序代码已准备好与数据库一起工作。现在需要配置数据源。

配置数据库

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

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

    Create database schema

  2. 使用以下代码更新 src/main/resources/schema.sql 文件:

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

    它创建了包含两列(idtext)的 messages 表。表结构与 Message 类的结构匹配。

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

    none
    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 请求向数据库添加消息

您应该使用 HTTP 客户端来与之前创建的端点一起工作。在 IntelliJ IDEA 中,使用嵌入式 HTTP 客户端:

  1. 运行应用程序。应用程序启动并运行后,您可以执行 POST 请求将消息存储到数据库中。

  2. 在项目根文件夹中创建 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/
  3. 执行所有 POST 请求。使用请求声明旁边的绿色 Run 图标。 这些请求将文本消息写入数据库:

    Execute POST request

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

    Execute GET requests

执行请求的另一种方式

您也可以使用任何其他 HTTP 客户端或 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 检索消息

扩展应用程序的功能,以通过 id 检索单个消息。

  1. 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
        }
    }
    vararg 实参在形参列表中的位置

    query() 函数接受三个实参:

    • 需要一个形参才能运行的 SQL 查询字符串
    • id,一个 String 类型的形参
    • RowMapper 实例,通过 lambda 表达式实现

    query() 函数的第二个形参被声明为 可变实参 (vararg)。在 Kotlin 中,可变实参形参的位置不要求是形参列表中的最后一个。

    singleOrNull() 函数

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

    用于通过其 id 获取消息的 .query() 函数是一个 Kotlin 扩展函数, 由 Spring Framework 提供。它需要额外导入 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<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() 函数所示:

    kotlin
    ResponseEntity

    ResponseEntity 代表 HTTP 响应,包括状态码、头信息和主体。它是一个通用封装器,允许您将自定义 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<String>) {
    runApplication<DemoApplication>(*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<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)
    }
}
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> =
        this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build()
}

运行应用程序

Spring 应用程序已准备好运行:

  1. 再次运行应用程序。

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

    http
    ### Get the message by its id
    GET http://localhost:8080/id
  3. 执行 GET 请求以从数据库中检索所有消息。

  4. Run 工具窗口中复制其中一个 id,并将其添加到请求中,如下所示:

    http
    ### Get the message by its id
    GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707

    请将您的消息 id 替换上述提到的 id。

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

    Retrieve message by its id

下一步

最后一步将向您展示如何使用 Spring Data 进行更常用的数据库连接。

继续下一章