Rails6 → Ktor へ移行した
JVMで言えば、今までSpringBootをやることが多かったのですが、今後使う可能性が高いということで、Ktorを使ってみました。
簡易的ということで、簡易にAPIサーバが作れるKtorと、簡易にDIが可能でありそうなKoinを使います。
また、Jetbrains製のORMを利用します(実際は、DAOとして書きますが)
バージョン
- Kotlin 1.3.7
- Ktor 1.3.2
- Koin 2.1.6
- Exposed 0.25.1
そんなにおかしなVerを使っているわけではないです。
構成
ピックアップしたコードだけ挙げます。コード全体は、https://qiita.com/ogasawaraShinnosuke/items/142be72f45fb4cb54072#code%E7%BD%AE%E3%81%8D%E5%A0%B4 こちらを見て下さい。
build.gradle
ktor,koinなどを追加します。また、gradle.propertiesでバージョンを一元管理しましょう。
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-core-jvm:$ktor_version"
implementation "io.ktor:ktor-client-apache:$ktor_version"
implementation "org.koin:koin-ktor:$koin_version"
}
注意点
以下の構文を追加して、jvmのターゲットを1.8にしないと、DI処理で落ちます。
参考 https://stackoverflow.com/a/50991772
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}
Dao
PostgreSQLで接続
命名がJavaっぽいのは、JVM系はJava、Groovyしか書いてなかった癖です。特に意味はありません。
Service
今回、DIとして使いたいものになります。
基本的にはKoinという素晴らしいものを使わせていただきます。
interface UserService {
fun getUsers(): List<UserDTO>
fun getUser(): User
}
class UserServiceImpl: UserService {
override fun getUsers(): List<UserDTO> {
val userDao = UserDao()
return userDao.findAll()
}
override fun getUser(id: Int): UserDTO {
val userDao = UserDao()
return userDao.findBy(id)
}
}
moduleを宣言する
後にKoinを利用することができます。
val serviceModule = module() {
singleBy<UserService, UserServiceImpl>()
}
koin利用
こちらで先程作ったモジュールをインストールします
install(Koin) {
modules(serviceModule)
}
Endpoint
APIはモジュール化することで、マイクロサービスにも適していると思われます。
routing {
get("/ping") {
call.respondText("Pong")
}
}
呼び出す時
val userService by inject<UserService>()
val users = userService.getUsers()
DAO
transaction内でしかDBにアクセスできないので、POJOなどにしてpayloadを戻す
ちなみに、ライブラリはJetbrain公式のExposedを使っている。念の為、このライブラリは0系なので仕様が変わる可能性が高いだろう。
val users: List<UserDTO> = transaction {
val users = UserEntity.all().map { it.entity2dto() }
return@transaction users
}
Entity to DTO
もっといいやり方があると思うが、一旦はサンプルコードということで
class Entity(id: EntityID<Int>): IntEntity(id) {
companion object: IntEntityClass<UserEntity>(Users)
// JOINは例えば var user by UserEntity referencedOn hogehoge.user みたいな感じ
// Payload格納は、entity2dtoなどで行うが、このあたりは正直ここでやりたくない
fun entity2dto(): UserDTO {
// DTOへ変換
}
}
Converterインターフェイスなどを作って、それをEntityに実装させるようにルール付けることで、JOIN関係もPayloadを返すことは可能である。このやり方が一般的かはわからないが、そもそもExposedのREADMEを見る限りだと大きくずれた書き方ではなさげ。
ORMに関しては、Javaの資産が使えるが、できればJetbrains製の成長を見守りたいし、PR見てる限りちゃんと反応くれそうだし、バグなども修正してくれやすいと思う。
設定ファイル
resources/application.conf
今回は、Hot Reloadにしてあるので、コンパイル後のフォルダを指定しています。
移行
すごく簡単にDIを実現できました。こんな素晴らしいものを作ってくださったことに感謝します。
実は、個人で作っているものが元々Rails6のAPIで作られていたのですが、Ktorに移行しました。パフォがかなり改善されたので、今度計測してみようと思います。
さらにJavaの資産が使えるというメリットは大きく、開発は好きな言語(Kotlin)で書き、jdbcなどをHikariCP(つまり、Javaで作られた有益なもの)など高速なものに変えるという様々なアプローチが可能です。
Kotlinは、NULL安全なので仮にDBから取れるデータがNULLの場合は、適宜Nullableにしないいけないし、新しくDDLを組む際は、NNでスキーマを作ることが求められている気がします。
また、Exposedもマイクロサービス化されていて、必要なライブラリをモジュールでGradleに記述することで無駄なライブラリをビルドしなくて良くなっています。このことから、例えば、datetimeなどの型が利用したい時には、別途Gradleに記述する必要があり、さらに、datetimeに関して言えば、以前まではデファクトだったJodaTimeかJava8以上に新機能として追加されたLocaltimeなどのどちらかを選択することが可能です。
Code置き場
読めば分かるのですが、KotlinでAPIサーバを簡易的に作ってみたというだけです。
コード読んだほうが早いという方はこちら。
書き方がおかしいとかあれば、何卒PRお願いします。あるべきコードを知りたいという意味もありますが、他の方が見た時に有益だと良いな、と思います。