いままで 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")
...
}
ノート本体を表現するクラスを用意。(lib/src/main/kotlin/storage/Note.kt)
data class Note(val uuid: String, val content: String)
ノートを保存するためのクラスを用意。(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 のインスタンスです。
NoteStorage に定義した add , get を使いたいのですが、その前にデータベースを用意しておく必要があります。
プロジェクトルートに storage.db というファイル名で、uuid, content のフィールドを持つテーブルを用意。
$ sqlite3 storage.db
sqlite> create table notes(uuid, content);
sqlite> .tables
notes
sqlite>
ctrl + D で sqlite プロンプトを終了。
ノートを追加して取得するテストを書きます。 (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 アプリケーション本体から データベース処理部分を切り離すことができそうです。