4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ktorで型安全なRoutingを提供する

Posted at

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

今回は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みたいなルートの作り方はイケてないことがよくわかった。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?