Home About Contact
SQLite , Note Taking , Kotlin

SQLite を使う、コネクションプールは HikariCP

いままで Spring Boot 経由で H2 Database Engine を使って個人的なメモを保存してきたが、 データベース部分だけを切り離して別モジュールにできないか、と考え始めた。 コネクションプールは Spring Boot の内部でも使用されているらしい HikariCP を使う。 おいおい FTS の機能も使ってみたいので、 データベースは SQLite を選択する。

環境

gradle

$ gradle --version

------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.9 (Private Build 17.0.9+9-Ubuntu-122.04)
OS:           Linux 5.15.0-91-generic amd64

sqlite3

 $ sqlite3 --version
3.37.2 2022-01-06

インストールされていない場合は sudo apt install sqlite3 すればよい。

プロジェクトを作成

$ mkdir storage
$ cd storage
$ gradle init

ライブラリプロジェクトとして言語は kotlin を選択。 グループとパッケージはデフォルトの storage のままでいきます。

lib/build.gradle.kts に必要なライブラリを追記。

dependencies {
  ...
  implementation("com.zaxxer:HikariCP:5.1.0")
  implementation("org.xerial:sqlite-jdbc:3.44.1.0")
  ...
}

Note

ノート本体を表現するクラスを用意。(lib/src/main/kotlin/storage/Note.kt)

data class Note(val uuid: String, val content: String)

NoteStorage

ノートを保存するためのクラスを用意。(lib/src/main/kotlin/storage/NoteStorage.kt)

package storage

import com.zaxxer.hikari.HikariDataSource
import com.zaxxer.hikari.HikariConfig

class NoteStorage(private val config: HikariConfig) {
  private val ds = HikariDataSource(config)
}

ノートを追加する関数を記述。

  val add: (Note)->Int = { note->
    val sql = "insert into notes (uuid, content) values (\"${note.uuid}\", \"${note.content}\")"

    ds.connection.use { conn->
      val stmt = conn.createStatement()
      stmt.executeUpdate(sql)
    }
  }

ノートを取得する関数を記述。

  val get: (String)->Note? = { uuid->
    val sql = "select content from notes where uuid=\"${uuid}\""

    ds.connection.use { conn->
      val stmt = conn.createStatement()
      val rs = stmt.executeQuery(sql)
      if( rs.next() ){ Note(uuid, rs.getString(1)) } else { null }
    }
  }

stmt.executeQuery(sql) で得られる rs は ResultSet のインスタンスです。

notes.db を作成

NoteStorage に定義した add , get を使いたいのですが、その前にデータベースを用意しておく必要があります。

プロジェクトルートに storage.db というファイル名で、uuid, content のフィールドを持つテーブルを用意。

$ sqlite3 storage.db
sqlite> create table notes(uuid, content);
sqlite> .tables
notes
sqlite>

ctrl + D で sqlite プロンプトを終了。

NoteStorageTest

ノートを追加して取得するテストを書きます。 (lib/src/test/kotlin/storage/NoteStorageTest.kt)

package storage

import kotlin.test.Test
import kotlin.test.assertTrue

import java.util.UUID
import com.zaxxer.hikari.HikariConfig

class NoteStorageTest {
  companion object {
    val createConfig: (String)->HikariConfig = { dbPath->
      HikariConfig().apply {
        driverClassName = "org.sqlite.JDBC"
        jdbcUrl = "jdbc:sqlite:${dbPath}"
      }
    }
  }

  @Test fun noteTest() {
    val dbPath = "../storage.db"
    val noteStorage = NoteStorage(createConfig(dbPath))
  
    val note = Note(
      UUID.randomUUID().toString(),
      "Hello, World!")
  
    noteStorage.add( note )
  
    val content1 = noteStorage.get( note.uuid )
    val note1content = if( note1!=null ){ note1.content } else { "" }
  
    assertTrue(
      (note.content==content1content),
      "they should have same value")
  }
}

storage ライブラリはプロジェクトルートを基準とすると ./lib/ に配置されている。 したがって storage ライブラリのテストは カレントディレクトリ が ./lib/ になる。 そして storage.db ファイルは プロジェクトルートに配置している。 そのため jdbc url は jdbc:sqlite:../storage.db になる。

テストを実行

$ ./gradlew assemble
$ ./gradle test

リファクタリング

NoteStorage クラスを PrepareStatement を使って処理を記述しなおします。

add 関数,

  val add: (Note)->Int = { note->
    val sql = "insert into notes (uuid, content) values (?,?)"

    ds.connection.use { conn->
      conn.prepareStatement(sql).use { ps->
        ps.setString(1, note.uuid)
        ps.setString(2, note.content)
        ps.executeUpdate()
      }
    }
  }

get 関数、

  val get: (String)->Note? = { uuid->
    val sql = "select content from notes where uuid=?"

    ds.connection.use { conn->
      conn.prepareStatement(sql).use { ps->
        ps.setString(1, uuid)
        val rs = ps.executeQuery()

        if( rs.next() ){ Note(uuid, rs.getString(1)) } else { null }
      }
    }
  }

それぞれ conn.prepareStatement を使って与えられた値を扱うように修正しました。

./gradlew test して意図通り作動するか確認しましょう。

後始末

HikariDataSource を生成したら最後は close しなければいけない、とのこと。

NoteStorage.kt に close 関数を追加します。

  val close: ()->Unit = {
    if( !ds.isClosed() ){
      ds.close()
    }
  }

テスト側でも最後に close をコールしましょう。

  @Test fun noteTest() {
    ...
    noteStorage.close()
  }

データベースの内容の確認

テストを実行したあと、データベースの中を覗いてみましょう。

$ sqlite3 storage.db
sqlite> select * from notes;
d241029a-7685-45c7-8cbc-fd89e160f05d|Hello, World!

コマンドラインインタフェースがあるのは楽ですね。

まとめ

これで Spring Boot アプリケーション本体から データベース処理部分を切り離すことができそうです。