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を呼んで見ると以下のレスポンスが返ってきます.
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を呼んで見ると以下のように返ってきます. /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の削除) /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に接続しようと思います.