KtorでCRUDアプリを作成する ~STEP3 : DBに接続
この記事はKtorでCRUDアプリを作成することを目標としています.前回のSTEP2ではは簡単なリストを用意し,それらに対しCRUDを実装しました.
今回の目標
今回は前回のコードを元に実際に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の結果
Readの結果
Updateの結果
またここで一度直接databaseを見てみましょう.
mysql> select * from school.students; +----+------+ | id | name | +----+------+ | 1 | Taro | +----+------+ 1 row in set (0.01 sec)
正しくデータが書き換わっていることが確認できます. Deleteの結果 delete databaseを確認すると実際に消えてしまっていることがわかります.
mysql> select * from school.students; Empty set (0.00 sec)
まとめ
今回はrequryを使ってDBに対してのCRUD操作を実装しました.次回はCleanArchitectureの考えで実装を分離していこうと思います.