基本的なことはドキュメントに書いてあるのでそれに倣う。
今回はExposedを使ったプロジェクトに型安全なRoutingを当て込んでみる。
環境
このリポジトリに定義している。型安全でないブランチがanswear
で型安全にしたブランチがtype-safe-routing
だ。
この記事にも実装の一部は掲載するが、全体像はブランチを切り替えて見てほしい。
修正前
以下のようなRouting.kt
がある。
package example.koin
import example.koin.controller.ExposedController
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.ktor.ext.inject
fun Application.configureRouting() {
val exposedController by inject<ExposedController>()
routing {
get("/") {
call.respondText("Hello! hands on exposed!!")
}
get("/inorin"){
exposedController.getInorin(call)
}
get("/allEmployees"){
exposedController.getAllEmployees(call)
}
get("/allEmployeesNames"){
exposedController.getAllEmployeesNames(call)
}
get("/employeeNameOfGeneralOrAccounting"){
exposedController.getEmployeeNameOfGeneralOrAccounting(call)
}
get("/employeeBySorted"){
exposedController.getEmployeeBySorted(call)
}
get("/howManyApplyExpenseByEmployee"){
exposedController.getHowManyApplyExpenseByEmployee(call)
}
get("/howMuchExpenseByEmployee"){
exposedController.getHowMuchExpenseByEmployee(call)
}
get("/employeeLimitOffset"){
exposedController.getEmployeeLimitOffset(call)
}
get("/employeeNameAndDepartment"){
exposedController.getEmployeeNameAndDepartment(call)
}
get("/hasExpenseEmployeeNames"){
exposedController.hasExpenseEmployeeNames(call)
}
get("/hasExpenseEmployeeNamesWithBetween"){
exposedController.hasExpenseEmployeeNamesWithBetween(call)
}
get("/allEmployeeTypeAndNames"){
exposedController.getAllEmployeeTypeAndNames(call)
}
get("/allEmployeeTypeAndNamesDistinct"){
exposedController.getAllEmployeeTypeAndNamesDistinct(call)
}
get("/hasExpenseEmployeeIdAndNames"){
exposedController.getHasExpenseEmployeeIdAndNames(call)
}
get("/overExpenseEmployeeIdAndNames"){
exposedController.getOverExpenseEmployeeIdAndNames(call)
}
get("/existsOverExpenseEmployeeIdAndNames"){
exposedController.getExistsOverExpenseEmployeeIdAndNames(call)
}
get("/latestEmployeeIdByDepartmentId"){
exposedController.getLatestEmployeeIdByDepartmentId(call)
}
get("/employeeNamesAndEnrollmentStatus"){
exposedController.getEmployeeNamesAndEnrollmentStatus(call)
}
get("/concatEmployeeNames"){
exposedController.getConcatEmployeeNames(call)
}
get("/concatPartnerNames"){
exposedController.getConcatPartnerNames(call)
}
get("/employeeFirstNameStrByte"){
exposedController.getEmployeeFirstNameStrByte(call)
}
get("/employeeFirstNameCharLength"){
exposedController.getEmployeeFirstNameCharLength(call)
}
get("/employeeFirstNameCharLengthOver3"){
exposedController.getEmployeeFirstNameCharLengthOver3(call)
}
post("/insertUpdateDeleteEmployee"){
exposedController.insertUpdateDeleteEmployee(call)
}
put("/updateApplyExpenseEmployee"){
exposedController.updateApplyExpenseEmployee(call)
}
get("/allPartners"){
exposedController.getAllPartners(call)
}
get("/allPartnersNames"){
exposedController.getAllPartnersNames(call)
}
get("/partnerNameById"){
exposedController.getPartnerNameById(call)
}
get("/partnerNameByLikeKeyword"){
exposedController.getAllPartnersNamesByLikeKeyword(call)
}
get("/partnerBySorted"){
exposedController.getPartnerBySorted(call)
}
get("/partnerLimitOffset"){
exposedController.getPartnerLimitOffset(call)
}
get("/partnerNameAndDepartment"){
exposedController.getPartnerNameAndDepartment(call)
}
get("/partnerNamesAndEnrollmentStatus"){
exposedController.getPartnerNamesAndEnrollmentStatus(call)
}
post("/insertUpdateDeletePartner"){
exposedController.insertUpdateDeletePartner(call)
}
}
}
このうちのルート/partnerNameById
の実装を見る。
suspend fun getPartnerNameById(call: ApplicationCall){
val id = call.parameters["partnerId"]?.toIntOrNull() ?: -1
val message = selectService.selectPartnerById(id)
call.respondText(message)
}
以上から、現状のRouting.kt
の問題点を列挙する。
- Emplpoyee,Partnerに関するルートが自由に定義できてしまっている
- これらのルートが今は整理できているが、将来的に乱雑になる可能性がある
- 引数の型検証が必要になっている
- call.parameters["partnerId"]?.toIntOrNull() ?: -1が冗長
- nullの場合のテストコードが無駄に必要になっている
この辺りをType Safe Routing
を導入することでスマートに解決する。
準備
Type Safe Routing
を導入するためにはkotlinx.serialization
が必要になるのでその準備をまずしていく。
build.gradle.kts
に以下を追加する。
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project
plugins {
kotlin("jvm") version "1.9.0"
+ kotlin("plugin.serialization") version "1.9.0"
id("io.ktor.plugin") version "2.3.3"
}
group = "example.koin"
version = "0.0.1"
application {
mainClass.set("io.ktor.server.tomcat.EngineMain")
val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}
repositories {
mavenCentral()
}
dependencies {
// When Set up
implementation("io.ktor:ktor-server-core-jvm")
implementation("io.ktor:ktor-server-tomcat-jvm")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-config-yaml:2.3.3")
implementation("io.insert-koin:koin-core:3.3.3")
implementation("io.insert-koin:koin-ktor:3.4.3")
testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
+ // for serialization
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0")
+ implementation("io.ktor:ktor-server-resources:$ktor_version")
// for exposed
implementation("org.jetbrains.exposed", "exposed-core", "0.41.1")
implementation("org.jetbrains.exposed", "exposed-dao", "0.41.1")
implementation("org.jetbrains.exposed", "exposed-jdbc", "0.41.1")
implementation("org.jetbrains.exposed:exposed-java-time:0.41.1")
implementation("mysql:mysql-connector-java:8.0.33")
}
リソースクラスの作成
これが型安全なルーティングの実態。今回はsrc/resources/Partner.kt
を作成し、ルートとパラメーターを定義してみる。
package example.koin.resources
import io.ktor.resources.*
@Resource("/partners")
class Partner {
@Resource("all")
class all(val parent: Partner)
@Resource("names")
class names(val parent: Partner){
@Resource("all")
class all(val parent: names)
@Resource("{id}")
class byId(val parent: names, val id: Int)
}
}
これをRouting.kt
に適用する。すると/allPartners
, /allPartnersNames
, /partnerNameById
と同様の型安全なルートを/partners
, /partners/names/all
, /partners/names/1
のように扱えるようになる。
get("/allPartners"){
exposedController.getAllPartners(call)
}
get<Partner.all>{
exposedController.getAllPartners(call)
}
get("/allPartnersNames"){
exposedController.getAllPartnersNames(call)
}
get<Partner.names.all>{
exposedController.getAllPartnersNames(call)
}
get("/partnerNameById"){
val id = call.parameters["partnerId"]?.toIntOrNull() ?: -1
exposedController.getPartnerNameById(call, id)
}
get<Partner.names.byId>{
partner -> exposedController.getPartnerNameById(call, partner.id)
}
これにより型安全なルーティングがひとまずはできた。
post, putのようなbodyに詳細値を入れて送る場合
この場合はRecourceはルートを提供するのみで、bodyの値はContent-Type
によって取り出して扱う必要がある。
いくつか方法はあるが、型安全に扱うなら"Objects"になると思う。
おわりに
パス階層の作り方とかを意識できるので非常によさそう。
実際に置き換えてみたが、/allPartners
みたいなルートの作り方はイケてないことがよくわかった。