KtorでCRUDアプリを作成する ~STEP2: Create・Read・Update・Delete

前回はKtorを使って"Hello World"を返すRouting定義を作成までを行いました.今回はその続きを行います.

今回の目標

前回の"Hello World"を返す部分はCRUDのReadの部分に対応します.今回はReadの他にもCreate,Update,Deleteに対応する部分を実装していきます.最終的にはDBに接続するのですが今回はそこまでは行いません.またKtorでJsonを扱えるようにもします.

クラスを定義

今回はDBの代わりにシンプルなクラスを作成し,そのオブジェクトのリストに対してCRUD操作を実装していきます. 前回のコードに対して以下のようなクラス,リストを作成します.

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

val students = mutableListOf(Student(1, "Taro"), Student(2, "Hanako"), Student(3, "Yuta"))

data class Student(val id: Int, val name: String)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    routing {
        get("/hello") {
            call.respond("Hello World")
        }
    }
}

Jsonの設定

KtorはJsonを簡単に扱えるようにする仕組みを用意してくれています. まずはdependenciesにktor-gsonを追加します.(jacksonバージョンもあります.)

dependencies {
    ・
    ・
    ・
    implementation "io.ktor:ktor-gson:$ktor_version"
}

そしてinstallブロックを呼び出して以下のようにgsonをインストールします.

fun Application.module(testing: Boolean = false) {
    install(ContentNegotiation) { //Content-Typeに応じた変換に関する設定
        gson {
            // Gsonの設定
            setPrettyPrinting()
        }
    }
            ・
            ・
            ・
}

ContentNegotiationを引数で渡すことでContent-Typeに応じた変換に関する設定を追加することを表します.そしてgsonブロックを渡すことでgsonの使用を宣言します.gsonブロック内ではGsonの設定を記述できます.今回はプリティプリントだけ設定して置きます. これだけでJsonを扱えるようになります.またKtorは色々なfeatureをinstallして設定や機能を追加するような仕組みになっています.

Read

Jsonの設定をしたのでまずは前回のgetメソッドを書き換えてリストを返すようにしましょう.具体的には以下のコードになります.

routing {
    get("/students") { //helloからstudentsに変更しています
        call.respond(students)
    }
}

gson featureをinstallしたのでcall.respondにstudentsを渡すだけでstudentsがjsonフォーマットでレスポンスを返してくれます.実際にstudentsを呼んで見ると以下のレスポンスが返ってきます. f:id:harada-777:20200211215737p:plain

Create

次にCreateのハンドラを作成します.対応するHttpメソッドはPOSTです.今回はリクエストボディにJson文字列でStudentを受け取るようにします.Json文字列を受け取るコードは以下のようになります.

routing("/students") {
    post {
        val inputJson = call.receive<Student>()
           students.add(inputJson) //studentsに受け取った生徒を追加する.
            call.respond(input.id)
    }
}

リクエストボディはcall.receiveを呼ぶことで取得できます.今回はStudentの形で受け取るので型パラメータにStudentを渡しています. とりあえず正しく受け取れていることを確認する為にidを返しておきます.例として以下のHajimeを追加します.

{
  "id": 4,
  "name": "Hajime"
}

実際にpostを呼んで見ると以下のように返ってきます. f:id:harada-777:20200211215840p:plain /studentsのgetを呼んでHajimeが追加されていることを確認して下さい.

Update

続いてUpdateのハンドラを実装します.対応するHttpメソッドはPUTです.今回はidを指定して名前を変更できるようにします.パスパラメータでidを指定しクエリパラメータで名前を受け取ります.存在しないidを指定したときは404 NotFoundを返します.

put("students/{id}") {
    val id = call.parameters["id"]!!.toInt() //パスパラメータを受け取る
    val name = call.request.queryParameters["name"]!! //クエリパラメータを受け取る
    //指定したidが存在していれば削除をして,新しいデータを登録する(update)
    if (students.removeIf { it.id == id }) {
        students.add(Student(id, name))
        call.respond("$id is updated")
        return@put
    }
    // 指定したidが存在しなければ404 not foundを返す
    call.respond(HttpStatusCode.NotFound, "$id is not found")
}

パスパラメータを受け取るときはurlを定義するときにパラメータ部分を/{pathParam}とします.そしてcall.parameters["pathParam"]とすることでパスパラメータを受け取ります.クエリパラメータはcall.request.queryParameters["queryParam"]とすることで中身を受け取れます.今回これらのパラメータがnullのときなどのエラーハンドリングをしてないのですが,それについては後日補足します. id=3のYukoをMasakoに変更します. /studentsのgetを呼んでMasakoに変更されていることを確認して下さい.

Delete

続いてDeleteのハンドラを実装します.対応するHttpメソッドはDELETEです.指定したidのStudentを削除するようにします.存在しないidを指定した場合は404 not foundを返します.具体的にコードは以下の通りです.

delete("students/{id}") {
    val id = call.parameters["id"]!!.toInt() //パスパラメータを受け取る
    if (students.find { it.id == id } == null) {
        call.respond(HttpStatusCode.NotFound, "$id is not found")
        return@delete
    }
    students.removeIf { it.id == id }
    call.respond("$id is deleted")
}

Updateのときと同様にパスパラメータでidを受け取ります.実際に/students/3のdeleteを呼んでみます.(Yutaの削除) f:id:harada-777:20200211215850p:plain /studentsのgetを呼んでYutaが削除されたかを確認してみて下さい.

ResponseのJson

最後にまだJson形式でレスポンスを直していきます.以下のような単純なクラスを用意します.

data class JsonResponse(val message: String)

JsonResponseを使いまたメッセージを少し変更して最終的には以下のようにしました.

get("/students") {
    call.respond(students)
}

post("/students") {
    val inputJson = call.receive<Student>()
    students.add(inputJson)
    call.respond(JsonResponse("id ${inputJson.id} is created"))
}

put("students/{id}") {
    val id = call.parameters["id"]!!.toInt()
    val name = call.request.queryParameters["name"]!!
    if (students.removeIf { it.id == id }) {
        students.add(Student(id, name))
        call.respond(JsonResponse("id $id is updated"))
        return@put
    }
    call.respond(HttpStatusCode.NotFound, JsonResponse("id $id is not found"))
}

delete("students/{id}") {
    val id = call.parameters["id"]!!.toInt()
    if (students.removeIf { it.id == id }) {
        call.respond(JsonResponse("id $id is deleted"))
        return@delete
    }
    call.respond(HttpStatusCode.NotFound, JsonResponse("id$id is not found"))
}

まとめ

今回は単純なリストに対してCRUD操作を実現し,それぞれに対応するハンドラを作成しました.次回は実際にDBに接続しようと思います.