我們花費蠻多時間撰寫過 Exposed 和 Ktor 的整合使用方式
現在又多了一份中文文件可以參考了
可以參考文件 https://openaidoc.org/zh-Hant/ktor/server-integrate-database
這邊的範例建立了一個包含 enum
的 data class
類別來處理資料庫內容
package com.example.model
import kotlinx.serialization.Serializable
enum class Priority {
Low, Medium, High, Vital
}
@Serializable
data class Task(
val name: String,
val description: String,
val priority: Priority
)
為了之後的自動化測試方便,所以多建立了一層 TaskRepository
介面
package com.example.model
interface TaskRepository {
fun allTasks(): List<Task>
fun tasksByPriority(priority: Priority): List<Task>
fun taskByName(name: String): Task?
fun addTask(task: Task)
fun removeTask(name: String): Boolean
}
使用實作跟前面的作法差不多,比方說,我們要連接 PostgreSQL,可以撰寫 PostgresTaskRepository
如下
package com.example.model
import com.example.db.TaskDAO
import com.example.db.TaskTable
import com.example.db.daoToModel
import com.example.db.suspendTransaction
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere
class PostgresTaskRepository : TaskRepository {
override suspend fun allTasks(): List<Task> = suspendTransaction {
TaskDAO.all().map(::daoToModel)
}
override suspend fun tasksByPriority(priority: Priority): List<Task> = suspendTransaction {
TaskDAO
.find { (TaskTable.priority eq priority.toString()) }
.map(::daoToModel)
}
override suspend fun taskByName(name: String): Task? = suspendTransaction {
TaskDAO
.find { (TaskTable.name eq name) }
.limit(1)
.map(::daoToModel)
.firstOrNull()
}
override suspend fun addTask(task: Task): Unit = suspendTransaction {
TaskDAO.new {
name = task.name
description = task.description
priority = task.priority.toString()
}
}
override suspend fun removeTask(name: String): Boolean = suspendTransaction {
val rowsDeleted = TaskTable.deleteWhere {
TaskTable.name eq name
}
rowsDeleted == 1
}
}
在這個基礎之上,我們可以再寫一個針對自動化測試使用的 FakeTaskRepository
package com.example.model
class FakeTaskRepository : TaskRepository {
private val tasks = mutableListOf(
Task("cleaning", "Clean the house", Priority.Low),
Task("gardening", "Mow the lawn", Priority.Medium),
Task("shopping", "Buy the groceries", Priority.High),
Task("painting", "Paint the fence", Priority.Medium)
)
override fun allTasks(): List<Task> = tasks
override fun tasksByPriority(priority: Priority) = tasks.filter {
it.priority == priority
}
override fun taskByName(name: String) = tasks.find {
it.name.equals(name, ignoreCase = true)
}
override fun addTask(task: Task) {
if (taskByName(task.name) != null) {
throw IllegalStateException("Cannot duplicate task names!")
}
tasks.add(task)
}
override fun removeTask(name: String): Boolean {
return tasks.removeIf { it.name == name }
}
}
這裡面使用一個 mutableList
作為測試資料的來源
在運作自動化測試的時候,我們要將運作的 TaskRepository
實作設置為 FakeTaskRepository
application {
val repository = FakeTaskRepository()
configureSerialization(repository)
configureRouting()
}
這樣一來,後面的測試都會使用 FakeTaskRepository
,就可以在不接觸資料庫的狀況下順利進行了
package com.example
import com.example.model.Priority
import com.example.model.Task
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.testing.*
import kotlin.test.*
class ApplicationTest {
@Test
fun tasksCanBeFoundByPriority() = testApplication {
application {
val repository = FakeTaskRepository()
configureSerialization(repository)
configureRouting()
}
val client = createClient {
install(ContentNegotiation) {
json()
}
}
val response = client.get("/tasks/byPriority/Medium")
val results = response.body<List<Task>>()
assertEquals(HttpStatusCode.OK, response.status)
val expectedTaskNames = listOf("gardening", "painting")
val actualTaskNames = results.map(Task::name)
assertContentEquals(expectedTaskNames, actualTaskNames)
}
@Test
fun invalidPriorityProduces400() = testApplication {
application {
val repository = FakeTaskRepository()
configureSerialization(repository)
configureRouting()
}
val response = client.get("/tasks/byPriority/Invalid")
assertEquals(HttpStatusCode.BadRequest, response.status)
}
@Test
fun unusedPriorityProduces404() = testApplication {
application {
val repository = FakeTaskRepository()
configureSerialization(repository)
configureRouting()
}
val response = client.get("/tasks/byPriority/Vital")
assertEquals(HttpStatusCode.NotFound, response.status)
}
@Test
fun newTasksCanBeAdded() = testApplication {
application {
val repository = FakeTaskRepository()
configureSerialization(repository)
configureRouting()
}
val client = createClient {
install(ContentNegotiation) {
json()
}
}
val task = Task("swimming", "Go to the beach", Priority.Low)
val response1 = client.post("/tasks") {
header(
HttpHeaders.ContentType,
ContentType.Application.Json
)
setBody(task)
}
assertEquals(HttpStatusCode.NoContent, response1.status)
val response2 = client.get("/tasks")
assertEquals(HttpStatusCode.OK, response2.status)
val taskNames = response2
.body<List<Task>>()
.map { it.name }
assertContains(taskNames, "swimming")
}
}
今天的部分就到這邊,我們明天見!