こんにちは!
Kotlinはモダンな文法で気持ちよくコーディングできる言語ですが、サーバーサイドをKotlinで実装するのは大きな可能性があると思っています。
- 文法がモダンであり、Null安全など安全なコーディングのための機能がある
- JVM上で実行されるので、インタプリタ言語より実行速度が速い
- Javaのライブラリを活用できる
今回は3のJavaライブラリ活用について、爆速コネクションプーリングのHikariCPをKotlin(ktor)サーバに導入する話をします。
HikariCPとは
WEBサーバがデータベースに対して張ったコネクションを貯めておくことを、コネクションプーリングと呼ぶ。
HikariCPは、Javaで実装されたコネクションプーリングの中で最速!
軽量で安定しており、たくさんのデータベースに対応しています。
導入してみる
環境
kotlin 1.9.10
ktor 2.3.4
Intellij IDEA 2023
Intellijでktorプロジェクトを作成済みの状態から始めます。
まずは、build.gradle.ktsに以下のように記述していきます。
dependencies {
...
implementation("com.zaxxer:HikariCP:5.0.1")
implementation("org.postgresql:postgresql:42.6.0")
...
}
HikariCPとポスグレドライバがインストールされます。
続いて、テスト用のデータベースを用意します。
今回はDockerでポスグレを立てました。
version: "3"
services:
postgres:
image: postgres:latest
ports:
- "5432:5432"
environment:
- POSTGRES_USER=iroha
- POSTGRES_PASSWORD=1203
- POSTGRES_DB=root
docker-compose up -dで起動しておきます。
テーブルを作成します。
create table note
(
id serial primary key,
title text not null,
body text not null,
created_at timestamp with time zone not null
);
今回は自分用のノート(メモ帳)をイメージして、id(主キー)、タイトル、本文、作成日のあるテーブルを作成しました。
HikariCPを呼び出すサービスクラスを作成します。
package com.example.service
import com.zaxxer.hikari.*
import java.sql.Connection
object HikariService {
private val hds: HikariDataSource
// 初期化
init {
val config = HikariConfig().apply {
driverClassName = "org.postgresql.Driver"
jdbcUrl = "jdbc:postgresql://localhost:5432/postgres"
username = "iroha"
password = "1203"
}
hds = HikariDataSource(config)
}
// コネクションを取得
fun getConnection(): Connection {
return hds.connection
}
}
このクラスはシングルトンで良いので、object型で作成しています。
init時にHikariDataSourceの初期化を行なっています。
メソッド「getConnection()」で、HikariDataSourceからコネクションを取り出すことができます。
インターフェース定義を行います。
package com.example.model
data class Note(
val id: Int,
val title: String,
val body: String,
val createdAt: String
)
interface INoteService {
fun find(): List<Note>
fun find(id: Int): List<Note>
fun insert(note: Note)
fun update(note: Note)
fun delete(id: Int)
}
データベースのテーブルと対応するデータクラス「Note」と、
Noteに関してデータベースとやり取りをする「INoteService」インターフェースを作成しました。
NoteServiceの中身を実装していきます。
package com.example.service
import com.example.model.INoteService
import com.example.model.Note
import org.intellij.lang.annotations.Language
import java.sql.ResultSet
class NoteService : INoteService {
// 全件取得
override fun find(): List<Note> {
@Language("SQL")
val query = """
SELECT id, title, body, created_at FROM note ORDER BY id
"""
HikariService.getConnection().use { con ->
con.prepareStatement(query).use { ps ->
ps.executeQuery().use { rows ->
return generateSequence {
when {
rows.next() -> rowToNote(rows)
else -> null
}
}.toList()
}
}
}
}
// 指定のIDで取得
override fun find(id: Int): List<Note> {
@Language("SQL")
val query = """
SELECT id, title, body, created_at FROM note WHERE id = ?
"""
HikariService.getConnection().use { con ->
con.prepareStatement(query).use { ps ->
ps.setInt(1, id)
ps.executeQuery().use { rows ->
return generateSequence {
when {
rows.next() -> rowToNote(rows)
else -> null
}
}.toList()
}
}
}
}
// 登録
override fun insert(note: Note) {
@Language("SQL")
val query = """
INSERT INTO note (title, body, created_at) VALUES (?, ?, current_timestamp)
"""
HikariService.getConnection().use { con ->
con.prepareStatement(query).use { ps ->
ps.run {
setString(1, note.title)
setString(2, note.body)
execute()
}
}
}
}
// 更新
override fun update(note: Note) {
@Language("SQL")
val query = """
UPDATE note SET title = ?, body = ? WHERE id = ?
"""
HikariService.getConnection().use { con ->
con.prepareStatement(query).use { ps ->
ps.run {
setString(1, note.title)
setString(2, note.body)
setInt(3, note.id)
execute()
}
}
}
}
// 削除
override fun delete(id: Int) {
@Language("SQL")
val query = """
DELETE FROM note WHERE id = ?
"""
HikariService.getConnection().use { con ->
con.prepareStatement(query).use { ps ->
ps.setInt(1, id)
ps.execute()
}
}
}
private fun rowToNote(row: ResultSet): Note = Note(
row.getInt(1),
row.getString(2),
row.getString(3),
row.getTimestamp(4).toInstant().toString()
)
}
今回はORマッパーを使っていないので、SQLを直接書いています。
useを使うことで、ブロックを抜けると自動でクローズしてくれるので、開放忘れの心配はありません。
prepareStatementを使うことで、SQLインジェクションへの対策になります。
最後にApplication.ktにルーティングを書きます。
GET、POST、PUT、DELETEを備えた、REST APIにします。
package com.example
import com.example.model.INoteService
import com.example.model.Note
import com.example.service.NoteService
import io.ktor.http.*
import io.ktor.serialization.gson.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.slf4j.event.Level
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
.start(wait = true)
}
val noteService: INoteService = NoteService()
fun Application.module() {
install(CallLogging) {
level = Level.INFO
}
install(ContentNegotiation) {
gson {
}
}
routing {
get("/note") {
val id = call.request.queryParameters["id"]?.toIntOrNull()
val notes: List<Note> = when (id) {
null -> noteService.find()
else -> noteService.find(id)
}
call.respond(mapOf("notes" to notes))
}
post("/note") {
val note = call.receive<Note>()
noteService.insert(note)
call.respond(note)
}
put("/note") {
val note = call.receive<Note>()
noteService.update(note)
call.respond(note)
}
delete("/note") {
val id = call.request.queryParameters["id"]?.toIntOrNull() ?: throw BadRequestException("")
noteService.delete(id)
call.respond(HttpStatusCode.OK)
}
}
}
※今回はお試しが目的なので、バリデーションや例外処理などは最低限にしています。
まとめ
HikariCPお試しということで、簡単なREST APIを実装してみました。
Kotlin & Ktorだと、気持ちよくコーディングできていいですね。
次はベンチマークを取ってみたいです。