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の考えで実装を分離していこうと思います.