Kotlin製Webアプリケーションフレームワーク Ktor の使い方4 〜ExposedでDBアクセス〜

f:id:Naotsugu:20201208213355p:plain

blog1.mammb.com

の続きです。


入力フォームを外部ファイルに抜き出す

前回の続きから作業しますが、すこしゴチャゴチャしてきたので、入力フォームを外部ファイルに抜き出しておきます。

UserForm.kt を以下のように作成します。

package ktor.example

import io.ktor.http.*
import kotlinx.html.*
import kotlinx.serialization.Serializable

@Serializable
data class UserForm(
        val uname: String = "",
        val email: String = "",
        var error: String = "") {

    constructor(params: Parameters) : this(
            params["uname"] ?: "",
            params["email"] ?: "") {
        error = if (uname.isBlank() || email.isBlank())
            "Name and Email is required."
        else
            "";
    }

    fun hasError() = error.isNotBlank()

    fun html(): HTML.() -> Unit = {
        head {
            link(rel = "stylesheet",
                    href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css")
        }
        body {
            div(classes = "container") {
                if (hasError()) {
                    p(classes = "text-danger") { +error }
                }
                postForm(action = "/user/new",
                         encType = FormEncType.applicationXWwwFormUrlEncoded) {
                    div {
                        +"Name:"
                    }
                    div {
                        textInput(name = "uname", classes = "form-control") { value = uname }
                    }
                    div {
                        +"Email:"
                    }
                    div {
                        textInput(name = "email", classes = "form-control") { value = email }
                    }
                    br
                    div {
                        submitInput(classes = "btn btn-outline-primary") { value = "Register" }
                    }
                }
            }
        }
    }
}

前回作成した入力フォームを外部に抜き出しただけです。


App.kt の該当箇所は以下のようになります。

// ...
fun Routing.root() {
    route("/user/new") {
        get {
            call.respondHtml(block = UserForm().html())
        }
        post {
            val form = UserForm(call.receiveParameters())
            if (form.hasError()) {
                call.respondHtml(block = form.html() )
            } else {
                call.respond(User(form.uname, form.email))
            }
        }
    }
}


Exposed でデータベースアクセス

入力フォームで受け付けた内容をデータベースに保存しましょう。

ここでは、JetBrains による ORMライブラリである Exposed を使っていきます。

依存には以下のように exposedh2 を追加します。

dependencies {
    // ...
    implementation("org.jetbrains.exposed:exposed:0.17.7")
    implementation("com.h2database:h2:1.4.200")
}


Exposed は、SQL DSL によりデータベース操作を行う方法と、エンティティオブジェクト経由でデータベース操作を行う方法があります。

今回は エンティティ によるデータベースアクセスを行います。


最初にデータベースのテーブル定義を作成します。

LongIdTable() を継承したオブジェクトでテーブルカラムを定義定義します。

object Users : LongIdTable() {
    val name  = varchar("name", 50).index()
    val email = varchar("email", 50)
}

このテーブル定義は以下のようなDDLに相当します。

CREATE TABLE IF NOT EXISTS USERS (ID BIGINT AUTO_INCREMENT PRIMARY KEY, "NAME" VARCHAR(50) NOT NULL, EMAIL VARCHAR(50) NOT NULL);
CREATE INDEX USERS_NAME ON USERS ("NAME");


該当テーブルに対応するエンティティは以下のように作成します。

class User(id: EntityID<Long>) : LongEntity(id) {
    companion object : LongEntityClass<User>(Users)
    var name by Users.name
    var email by Users.email
}

テーブル定義の内容をエンティティにマッピングします。


データベース操作

今回はインメモリの H2 を利用するので接続は以下のようになります。

    Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
            driver = "org.h2.Driver", user = "sa", password = "")
    transaction {
        SchemaUtils.create(Users)
    }

SchemaUtils.create() で指定したテーブル定義に従ったテーブル生成を行うことができます。

データベースアクセスは transaction{ } の中で行います。User のインサートは以下のようなコードになります。

transaction {
    val user = User.new {
        name = "Thom"
        email = "thom@example.com"
    }
}

検索は以下のように行うことができます。

User.find { Users.name like "Thom" }


データベース操作

前回までに作成した拡張関数 main() を以下のように編集します。

fun Application.main() {
    install(ContentNegotiation) {
        json()
    }
    routing {
        root()
    }
    Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
            driver = "org.h2.Driver", user = "sa", password = "")
    transaction {
        SchemaUtils.create(Users)
    }
}


root() を以下のように編集します。

fun Routing.root() {
    route("/user/new") {
        get {
            call.respondHtml(block = UserForm().html())
        }
        post {
            val form = UserForm(call.receiveParameters())
            if (form.hasError()) {
                call.respondHtml(block = form.html())
            } else {
                call.respond(
                    transaction {
                        User.new {
                            name = form.uname
                            email = form.email
                        }.dto()
                    }
                )
            }
        }
    }
}

入力フォームで受け取った内容でデータベースに永続化しています。


Exposed で作成したエンティティはそのまま JSON にシリアライズできません。

そのため、Userdto() で DTO を生成するようにしています。

transaction {
    User.new {
        name = form.uname
        email = form.email
    }.dto()
}

DTO は以下のように定義します。

@Serializable
data class UserDto(val id: Long, val name: String, val email: String)

そして、User には dto() メソッドとして以下のように定義します。

class User(id: EntityID<Long>) : LongEntity(id) {
    companion object : LongEntityClass<User>(Users)
    var name by Users.name
    var email by Users.email

    fun dto() = UserDto(id.value, name, email)
}


実行

ではアプリケーションを実行してみましょう。

$ ./gradlew run

f:id:Naotsugu:20201218225508p:plain

Register により以下の SQL が発行されます。

INSERT INTO USERS (EMAIL, "NAME") VALUES ('thom@example.com', 'Thom')

ID が 1 の User が登録されます。

f:id:Naotsugu:20201218225601p:plain


App.kt は以下のようになりました。

package ktor.example

import io.ktor.application.*
import io.ktor.features.*
import io.ktor.html.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.dao.*
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction

@Serializable
data class UserDto(val id: Long, val name: String, val email: String)

object Users : LongIdTable() {
    val name  = varchar("name", 50).index()
    val email = varchar("email", 50)
}

class User(id: EntityID<Long>) : LongEntity(id) {
    companion object : LongEntityClass<User>(Users)
    var name by Users.name
    var email by Users.email
    fun dto() = UserDto(id.value, name, email)
}

fun Application.main() {
    install(ContentNegotiation) {
        json()
    }
    routing {
        root()
    }
    Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
            driver = "org.h2.Driver", user = "sa", password = "")
    transaction {
        SchemaUtils.create(Users)
    }
}

fun Routing.root() {
    route("/user/new") {
        get {
            call.respondHtml(block = UserForm().html())
        }
        post {
            val form = UserForm(call.receiveParameters())
            if (form.hasError()) {
                call.respondHtml(block = form.html())
            } else {
                call.respond(
                    transaction {
                        User.new {
                            name = form.uname
                            email = form.email
                        }.dto()
                    }
                )
            }
        }
    }
}



次回に続く

Kotlin Cookbook: A Problem-Focused Approach (English Edition)

Kotlin Cookbook: A Problem-Focused Approach (English Edition)

  • 作者:Kousen, Ken
  • 発売日: 2019/11/14
  • メディア: Kindle版