【サーバーサイドKotlin】HikariCPを試してみる

こんにちは！
Kotlinはモダンな文法で気持ちよくコーディングできる言語ですが、サーバーサイドをKotlinで実装するのは大きな可能性があると思っています。

1. 文法がモダンであり、Null安全など安全なコーディングのための機能がある
1. JVM上で実行されるので、インタプリタ言語より実行速度が速い
1. Javaのライブラリを活用できる

今回は3のJavaライブラリ活用について、爆速コネクションプーリングのHikariCPをKotlin（ktor）サーバに導入する話をします。

## HikariCPとは

https://github.com/brettwooldridge/HikariCP

WEBサーバがデータベースに対して張ったコネクションを貯めておくことを、コネクションプーリングと呼ぶ。
HikariCPは、Javaで実装されたコネクションプーリングの中で最速！
軽量で安定しており、たくさんのデータベースに対応しています。

## 導入してみる

環境
```
kotlin 1.9.10
ktor 2.3.4
Intellij IDEA 2023
```
Intellijでktorプロジェクトを作成済みの状態から始めます。

まずは、build.gradle.ktsに以下のように記述していきます。

``` gradle:build.gradle.kts
dependencies {
    ...
    implementation("com.zaxxer:HikariCP:5.0.1")
    implementation("org.postgresql:postgresql:42.6.0")
    ...
}
```
HikariCPとポスグレドライバがインストールされます。

続いて、テスト用のデータベースを用意します。
今回はDockerでポスグレを立てました。

```docker-compose:docker-compose.yml
version: "3"
services:
  postgres:
    image: postgres:latest
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=iroha
      - POSTGRES_PASSWORD=1203
      - POSTGRES_DB=root
```
docker-compose up -dで起動しておきます。

テーブルを作成します。
```SQL
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を呼び出すサービスクラスを作成します。
```kotlin:HikariService.kt
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からコネクションを取り出すことができます。

インターフェース定義を行います。
```kotlin:Note.kt
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の中身を実装していきます。
```kotlin:NoteService.kt
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にします。
```kotlin:Application.kt
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だと、気持ちよくコーディングできていいですね。
次はベンチマークを取ってみたいです。