月曜日, 6月 16, 2025
- Advertisment -
ホームニューステックニュースKtorで型安全なRoutingを提供する #Kotlin - Qiita

Ktorで型安全なRoutingを提供する #Kotlin – Qiita



Ktorで型安全なRoutingを提供する #Kotlin - Qiita

基本的なことはドキュメントに書いてあるのでそれに倣う。

今回は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 injectExposedController>()
    routing {
        get("https://qiita.com/") {
            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)
        }
        getPartner.all>{
            exposedController.getAllPartners(call)
        }
        get("/allPartnersNames"){
            exposedController.getAllPartnersNames(call)
        }
        getPartner.names.all>{
            exposedController.getAllPartnersNames(call)
        }
        get("/partnerNameById"){
            val id = call.parameters["partnerId"]?.toIntOrNull() ?: -1
            exposedController.getPartnerNameById(call, id)
        }
        getPartner.names.byId>{
            partner -> exposedController.getPartnerNameById(call, partner.id)
        }

これにより型安全なルーティングがひとまずはできた。

この場合はRecourceはルートを提供するのみで、bodyの値はContent-Typeによって取り出して扱う必要がある。

いくつか方法はあるが、型安全に扱うなら”Objects”になると思う。

パス階層の作り方とかを意識できるので非常によさそう。

実際に置き換えてみたが、/allPartnersみたいなルートの作り方はイケてないことがよくわかった。





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -