KtorでCRUDアプリを作成する ~STEP3 : DBに接続

この記事はKtorでCRUDアプリを作成することを目標としています.前回のSTEP2ではは簡単なリストを用意し,それらに対しCRUDを実装しました.

oboe-note.hatenablog.com

今回の目標

今回は前回のコードを元に実際にrequeryと言うORマッパーを使用してDBに接続して行きたいと思います.(ORマッパーとはオブジェクトとDBの橋渡しをしてくれるものです)

MySQL DBの作成

今回ははMySQLを使ってDBを作成します.dockerを使って作成しましょう.dockerをインストールしていない方はインストールをお願いします.

docker run -rm --name mysql -e MYSQL_ROOT_PASSWORD=mysql -p 3306:3306 -d mysql:latest

作成したDBに接続します.

mysql -h 127.0.0.1 --port 3306 -uroot -pmysql 

次にschoolというdatabeと前回作成したStudentクラスに対応するstudents tableを作成します.

mysql> create database school;
mysql> create table school.students(id int PRIMARY KEY, name varchar(100) );

これでstudents tableを作成することができました.これを使って実際にDBに接続を行うKotlinコードを書いていきます.

requeryを依存関係に追加

build.gradleのdependenciesにrequeryを以下のように追加しましょう.

dependencies {
        ・
        ・
    compile 'io.requery:requery:1.6.1'
    compile 'io.requery:requery-kotlin:1.6.1'
    kapt 'io.requery:requery-processor:1.6.1'
    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.19' //今回はMySQLを使用するため 
}

またAnnotation Processing(Annotaionによるコードの自動生成)をKotlinで使用できるようにするため,build.gradleに以下を追加します.

apply plugin: 'kotlin-kapt'

以上がgradleの定義です.

DBの接続定義

次にrequeryを使ってMySQLのDBに接続するための定義を書いて行きます.

//今回はApplication.ktに記述
val dataSource = MysqlDataSource().apply {
    serverName = "127.0.0.1"
    port = 3306
    user = "root"
    password = "mysql"
    databaseName = "school"
}
val dataStore = KotlinEntityDataStore<Persistable>(KotlinConfiguration(dataSource = dataSource, model = Models.DEFAULT))

以上のようにdataStoreを生成することでdataStoreを通してDBに接続することができます.違うDBを使用するときはコンストラクタで渡すdataSourceを変えれば良いです. またPersitableはエンティティクラス(DBのテーブルに対応したクラス)を自動生成するためのInterfaceであり,これをKotlinEntityDataStoreに型パラメータとして渡します.またこれを継承したクラスは自動でエンティティクラスが生成される仕組みになっており,Models.Defaultもまた自動生成されます.

エンティティの作成

次にエンティティクラスを生成する為の定義を記述します.エンティティクラスを生成するためにApplication.ktに以前記述したStudentクラスを別ファイルに記述します.

// Student.kt
package com.example

import io.requery.Entity
import io.requery.Key
import io.requery.Persistable
import io.requery.Table

@Entity
@Table(name = "students")
data class Student(
    @get:Key
    var id: Int,
    var name: String
) : Persistable

エンティティを生成したい対象クラスには@Entity@Table(name="対象のtable名")アノテーションを付けます.そしてPersistableを継承します.こうすることでエンティティクラスが自動生成されます.

DAO (Data Access Object)の定義

DAOとはデータを操作する為のメソッドを提供するオブジェクトです.実際のコードは以下になります.

package com.example

import io.requery.kotlin.EntityStore
import io.requery.Persistable
import io.requery.kotlin.eq

class StudentDao {
    //students tableに入っているレコードを全て取得する
    fun findAll(dataStore: EntityStore<Persistable, Any>): List<Student> = dataStore.select(Student::class).get().toList()
    //students tableに1つレコードを追加する
    fun create(student: Student, dataStore: EntityStore<Persistable, Any>) {
        dataStore.insert(student)
    }
    //指定されたstudents tableのレコードを1つ更新する
    fun update(student: Student, dataStore: EntityStore<Persistable, Any>) {
        dataStore.update(student)
    }
    //指定されたstudents tableの1つレコードを1つ削除する
    fun delete(id: Int, dataStore: EntityStore<Persistable, Any>): Int =
        dataStore.delete(Student::class).where(Student::id eq id).get().value()
}

今回はEntityStoreをメソッドの引数で渡す実装にしました.こうすることで,DAOの外側でトランザクション境界を貼ることができるからです.これについては後ほど説明します.

前回のCRUD操作を実際のDAOに置き換える

以前リストに対して行なっていた操作を今回作成したDAOに置き換えます.書き換えたコードは以下のようになります.

//Application.kt
fun Application.module(testing: Boolean = false) {
    val dao = StudentDao()
    install(ContentNegotiation) {
        gson {
            setPrettyPrinting()
        }
    }
    routing {
        //Read
        get("/students") {
            call.respond(dao.findAll(dataStore))
        }
        //Create
        post("/students") {
            val inputJson = call.receive<Student>()
            students.add(inputJson)
            dataStore.withTransaction {
                dao.create(inputJson, this)
            }
            call.respond(JsonResponse("id ${inputJson.id} is created"))
        }
        //Update
        put("students/{id}") {
            val id = call.parameters["id"]!!.toInt()
            val name = call.request.queryParameters["name"]!!
            val students = dao.findAll(dataStore)

            if (students.indexOfFirst { it.id == id } < 0) {
                call.respond(HttpStatusCode.NotFound, JsonResponse("id $id is not found"))
                return@put
            }

            dataStore.withTransaction {
                dao.update(Student(id, name), this)
            }
            call.respond(JsonResponse("id $id is updated"))
        }
        //Delete
        delete("students/{id}") {
            val id = call.parameters["id"]!!.toInt()
            val students = dao.findAll(dataStore)

            if (students.indexOfFirst { it.id == id } < 0) {
                call.respond(HttpStatusCode.NotFound, JsonResponse("id $id is not found"))
                return@delete
            }

            dataStore.withTransaction {
                dao.delete(id, this)
            }
            call.respond(JsonResponse("id $id is deleted"))
        }
    }
}

PutとDeleteに関してはstudentsテーブルのレコードを取得し,そのidのstudentがいるかを判定し,存在しなければ404 not found を返しています. またCreate Put Deleteに関してはDBを書き換えるのでトランザクションを張っています.requeryではこのようにwithTransactionブロックで囲い,このthis(BlockingEntityStoreのインスタンス)を使って操作を行うとブロックが閉じたときに操作がコミットされます.DAOのメソッドにBlockingEntityStoreのインスタンスを渡せるように実装したのは,このようにDAOの外側でトランザクション境界を貼れるようにする為です. 実際に呼んでみましょう. Createの結果

f:id:harada-777:20200310222346p:plain

Readの結果

f:id:harada-777:20200310222030p:plain

Updateの結果

f:id:harada-777:20200310222027p:plain

またここで一度直接databaseを見てみましょう.

mysql> select * from school.students;
+----+------+
| id | name |
+----+------+
|  1 | Taro |
+----+------+
1 row in set (0.01 sec)

正しくデータが書き換わっていることが確認できます. Deleteの結果 deleteAlt text databaseを確認すると実際に消えてしまっていることがわかります.

mysql> select * from school.students;
Empty set (0.00 sec)

まとめ

今回はrequryを使ってDBに対してのCRUD操作を実装しました.次回はCleanArchitectureの考えで実装を分離していこうと思います.

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に接続しようと思います.

KtorでCRUDアプリを作成する ~STEP1: Hello World

修論が終わりつつあり,余裕があるので1年ぶりの初投稿します笑  全体の目標としてははKotlin製のFrameworkであるKtorを使って,CRUD(Create, Read, Delete, Update)が可能なWebアプリケーションを作成することです.KtorはSpringほどの機能はないですがシンプルなので使いやすいFrameworkです. 記事は段階的に書こうと思っています.今回はその初回です.

今回の目標

今回はKtorでアプリケーションを起動してレスポンスでHello worldを返せるようにすることです.

Ktor Pluginのインストール

今回はIntellijを使って作成しようと思います.IntellijにはKtorをサポートするPluginが提供されているのでまずはそのプラグインをインストールして下さい. f:id:harada-777:20200209145113p:plain

プロジェクトの作成

IntellijのCreate New ProjectからKtorを選びプロジェクト作成して下さい.デフォルト設定のまま作成して貰って大丈夫です. f:id:harada-777:20200209145026p:plain もしうまく作成しなければgradle/wrapper/gradle-wrapper.properties を書き換えるなどをしてgradleのversionをあげて下さい.(僕は6.1.1で起動しました.)

Hello Worldを返す

プロジェクトがうまく作成されるとApplication.ktというfileがあると思います.このファイルを書き換えてGetに対するレスポンスでHello worldを返そうと思います. Routingを定義する方法はいくつかあるのですが今回は標準のものを使います.例を見て頂いた方が早いので以下に書き換えた後のApplication.ktを示します.

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

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    routing { //
        get("/hello") { //get methodを定義する
            call.respond("Hello World") //responseの中身を定義する
        }
    }
}

このような形でrouting内にどのパスに対してどんなメソッドを定義するかを記述します.どんなレスポンスを返すかはcall.respondeの引数で指定します. アプリケーションはデフォルト設定ではlocalhost:8080で起動するので実際に叩いてみると以下のようなレスポンスが返ってきます. f:id:harada-777:20200209145141p:plain

まとめ

今回はKtorでHello Worldを返す部分までを実現しました.次回は他のHTTPメソッドの定義やクエリパラメータの受け取り方等について触れようと思っています

oboe-note.hatenablog.com

はじめまして

こんにちは。

私は神戸に住む大学院生です。研究をしながら開発インターンをしています。そこで勉強したことや覚えたことをまとめようと思いブログをはじめました!

これからたまに更新するのでよろしくお願いします。

現在は株式会社ユーザベースでソフトウェアエンジニアとして働いています.(2020卒)

ほぼ業務に関連した技術の勉強のアウトプットを行っているので、もしご興味のある方は

Uzabase, Inc. - Current Openingsまで!