の続きです。
入力フォームを外部ファイルに抜き出す
前回の続きから作業しますが、すこしゴチャゴチャしてきたので、入力フォームを外部ファイルに抜き出しておきます。
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 を使っていきます。
依存には以下のように exposed
と h2
を追加します。
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 にシリアライズできません。
そのため、User
に dto()
で 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
Register
により以下の SQL が発行されます。
INSERT INTO USERS (EMAIL, "NAME") VALUES ('thom@example.com', 'Thom')
ID が 1 の User が登録されます。
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)
- 作者:Kousen, Ken
- 発売日: 2019/11/14
- メディア: Kindle版