為 Spring Boot 專案新增資料庫支援
這是**Spring Boot 與 Kotlin 入門指南**教學課程的第三部分。在繼續之前,請確保您已完成先前步驟:
使用 Kotlin 建立 Spring Boot 專案
為 Spring Boot 專案新增資料類別
為 Spring Boot 專案新增資料庫支援
使用 Spring Data CrudRepository 進行資料庫存取
在本教學課程的此部分中,您將使用 Java 資料庫連線 (JDBC) 為專案新增和配置資料庫。 在 JVM 應用程式中,您使用 JDBC 與資料庫互動。 為方便起見,Spring 框架提供了 JdbcTemplate
類別,它簡化了 JDBC 的使用並有助於避免常見錯誤。
新增資料庫支援
基於 Spring 框架的應用程式的常見做法是在所謂的_服務層級 (service layer)_ 中實作資料庫存取邏輯 – 這是業務邏輯 (business logic) 所在之處。 在 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
}
}
建構函式引數與依賴注入 (Dependency Injection) – (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 轉換之後,查詢函式最終有兩個引數:第一個位置是 String,最後一個位置是 Lambda 運算式。根據 Kotlin 慣例,如果函式的最後一個參數是函式,則作為相應引數傳遞的 Lambda 運算式可以放在括號外。這種語法也稱為 後置 Lambda 運算式 (trailing 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) 並設定位置標頭,指示所建立資源的上下文路徑。
更新 MessageService 類別
Message
類別的 id
被宣告為可空字串 (nullable String):
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 );
它會建立具有兩個欄位:
id
和text
的messages
表。該表格結構與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 請求。使用請求宣告旁裝訂線中的綠色 Run 圖示。 這些請求會將文字訊息寫入資料庫:
執行 GET 請求並在 Run 工具視窗中查看結果:
執行請求的替代方法
您也可以使用任何其他 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 查詢字串
- 型別為 String 的參數
id
- 由 Lambda 運算式實作的
RowMapper
實例
參數列表中的可變引數 (vararg) 位置
query()
函式接受三個引數:query()
函式的第二個參數被宣告為可變引數 (vararg
)。在 Kotlin 中,可變引數參數的位置不要求必須是參數列表中的最後一個。`singleOrNull()` 函式
singleOrNull()
函式回傳單一元素,如果陣列為空或有多個具有相同值的元素,則回傳null
。DANGER
用於依 ID 擷取訊息的
.query()
函式是 Spring 框架提供的 Kotlin 擴充函式。它需要額外的導入
import org.springframework.jdbc.core.query
,如上述程式碼所示。將帶有
id
參數的新getMessage(...)
函式新增到MessageController
類別中: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 框架從上下文路徑中擷取的,因為您使用@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 請求以從資料庫中擷取所有訊息。
在 Run 工具視窗中複製其中一個 ID,並將其新增到請求中,如下所示:
http### Get the message by its id GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707
NOTE
請將您的訊息 ID 替換掉上述的範例 ID。
執行 GET 請求並在 Run 工具視窗中查看結果:
下一步
最後一步將向您展示如何使用 Spring Data 進行更流行的資料庫連線。