2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-09-09

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

  1. 文法がモダンであり、Null安全など安全なコーディングのための機能がある
  2. JVM上で実行されるので、インタプリタ言語より実行速度が速い
  3. 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に以下のように記述していきます。

build.gradle.kts
dependencies {
    ...
    implementation("com.zaxxer:HikariCP:5.0.1")
    implementation("org.postgresql:postgresql:42.6.0")
    ...
}

HikariCPとポスグレドライバがインストールされます。

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

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で起動しておきます。

テーブルを作成します。

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を呼び出すサービスクラスを作成します。

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からコネクションを取り出すことができます。

インターフェース定義を行います。

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の中身を実装していきます。

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にします。

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だと、気持ちよくコーディングできていいですね。
次はベンチマークを取ってみたいです。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?