🫖

Kotlin + Gradle + JDBC でCRUD処理を実行する

2022/06/03に公開

概要

Kotlin の勉強をしたかったのですが、Kotlin と JDBC(SpringJDBC)を用いて、CRUD をする記事があまり見つかりませんでした。
「Kotlin で簡単な CRUD 操作を知りたい」「JDBC みたいな実装が薄い DB クライアントを使いたい」「MyBatis は実装が複雑」といった目的のために作成しました。
素の JDBC ではなく、SpringJDBC を用いているのは、Web フレームワークである Spring は今後使う可能性が高いので、Spring 前提で記事を作成しました。

完成したソースコードは以下になります。

https://github.com/Msksgm/kotlin-gradle-jdbc-demo

前提

プログラミング言語と DB は以下の構成で実装します。
Kotlin と PostgrSQL の基本的なこと、Intellij の使い方については知っている前提です。

項目 要素
プログラミング言語 Kotlin
DB PostgreSQL
OS macOS
IDE Intellij IDEA 2022.1.1(Apple Silicon 版)

本記事では、Spring Initializr を用いて初期環境を作成しますが、Dependencies に SpringBoot Web(Spring MVC)を使用しません。
JDBC の使い方と、SpringBoot の使い方を切り分けるためです。

環境構築

Spring Initializr

Spring Initializr を用いて、ひな型を作成します。
以下のリンクから、Spring Initializr の画面を開いてください。

https://start.spring.io/

設定は以下にします。

項目 要素
Project Gradle Project
Language Kotlin
Spring Boot 2.7.0
Project Metadata Artifact kotlin-gradle-jdbc-demo
Project Metadata Name kotlin-gradle-jdbc-demo
Packaging Jar
Java 17
Dependencies PostgreSQL Driver、JDBC API

spring_initializer
Spring Initializr の設定画面

「Generate」を押下し、zip ファイル(kotlin-gradle-jdbc-demo.zip)がダウンロードします。
適当なディレクトリに移動します。
そして、Intellij でプロジェクトを開いてください。初回は依存ツールのインストールのため時間がかかります。
何もしていなければ以下のようなディレクトリ構成になっています。

> tree -L 2
.
├── HELP.md
├── build.gradle.kts
├── gradle
│   └── wrapper
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    └── test

5 directories, 5 files

DB

Kotlin と JDBC の環境構築は終わりましたが、JDBC はアプリケーション起動時に、DB との接続が確立していなければ、アプリケーションが落ちてしまいます。
docker-compose を用いて、DB の環境構築をします。
プロジェクトのルートディレクトリにdocker-compose.yamlを作成して、以下の記述をしてください。

./docker-compose.yaml
---

version: '3.8'

services:
  #
  # PostgreSQL
  #
  sample-pg:
    image: postgres:14-bullseye
    container_name: sample-pg
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: sample-user
      POSTGRES_PASSWORD: sample-pass
      POSTGRES_DB: sample-db
      POSTGRES_INIT_DB_ARGS: --encoding=UTF-8
    volumes:
      - type: bind
        source: ${PWD}/sql/
        target: /docker-entrypoint-initdb.d/

続いて、DB に初期データを挿入するための、SQL を作成します。
./sqlディレクトリを作成し、001-customer.sqlに以下の SQL を記述してください。

./sql/001-customer.sql
DROP TABLE IF EXISTS customer;
CREATE TABLE IF NOT EXISTS customer (
  id SERIAL,
  first_name VARCHAR(255),
  last_name VARCHAR(255)
);

INSERT INTO customer ( first_name, last_name ) VALUES
  ( 'Alice', 'Sample1' )
  , ( 'Bob', 'Sample2' )
;

DB の起動を確認します。以下のコマンドで起動できなかったら、docker の設定を確認してください。

DBの起動確認
# DB を起動
docker compose up sample-pg
# DBを停止 & 削除
# docker compose down

最後に JDBC と DB の接続設定を追加します。

./src/main/resources/application.properties
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/sample-db
spring.datasource.username=sample-user
spring.datasource.password=sample-pass
spring.datasource.driver-class-name=org.postgresql.Driver

以上で環境構築は完了です。

実装

これから、Kotlin を用いた CRUD 実装します。
簡略化のため、./src/main/kotlin/com/example/kotlingradlejdbcdemo配下にすべてのファイルを配置します。

Create

まず、Create からできるようにします。
jdbc を用いた SQL の実行は簡潔で、副作用がある処理はupdateで、副作用がない処理はqueryqueryForStream)で実行可能です。
今回は、INSERT 処理(副作用がある処理)なのでjdbcTemaplate.updateを使用します。
(Intellij にはProperty 'jdbcTemplate' could be privateと怒られますが、今回の本質ではないので無視します。@Componentをつければ消えます)。

./src/main/kotlin/com/example/kotlingradlejdbcdemo/InsertCommand.kt
package com.example.kotlingradlejdbcdemo

import org.springframework.jdbc.core.JdbcTemplate

interface InsertCommand {
    fun perform(firstName: String, lastName: String)
}

class InsertCommandImpl(val jdbcTemplate: JdbcTemplate) : InsertCommand {
    val sql = "INSERT INTO customer(first_name, last_name) VALUES (?, ?);"
    override fun perform(firstName: String, lastName: String) {
        jdbcTemplate.update(sql, firstName, lastName)
    }
}

Read

続いて、Read についてです。
最初に data class を用いて Customer クラスを実装します。

./src/main/kotlin/com/example/kotlingradlejdbcdemo/Customer.kt
package com.example.kotlingradlejdbcdemo

data class Customer(
    val id: Long,
    val firstName: String,
    val lastName: String,
)

続いて、Customer クラスと DB の O/R マッパとなる CustomRowMapper クラスを作成します。
jdbc に用意されている、RowMapperインタフェースを用いることで、簡単に DB のカラムと対応づけが可能です。

package com.example.kotlingradlejdbcdemo
import org.springframework.jdbc.core.RowMapper
import java.sql.ResultSet

class CustomerRowMapper : RowMapper<Customer> {
    override fun mapRow(rs: ResultSet, rowNum: Int): Customer? {
        return Customer(
            rs.getLong("id"),
            rs.getString("first_name"),
            rs.getString("last_name"),
        )
    }
}

最後に、これらのクラスを組み合わせて、テーブル内のすべてのクラスを取得する処理を実装します。
基本的には Read を実装したときと同様です。
複数のカラムを取得したときには、jdbcTemplate.queryForStream()を使用します。
jdbcTemplate.queryForStream()が返す型はStreamのため、perform()の戻り値をList<Customer>にする必要があります。
そこで、toList()を文末につけることで変換されます。

package com.example.kotlingradlejdbcdemo

import org.springframework.jdbc.core.JdbcTemplate

interface SelectAllQuery {
    fun perform(): List<Customer>
}

class SelectAllQueryImpl(val jdbcTemplate: JdbcTemplate) : SelectAllQuery {
    val sql = """
        SELECT id, first_name, last_name
        FROM customer;
    """.trimIndent()
    val mapper = CustomerRowMapper()
    override fun perform(): List<Customer> {
        return jdbcTemplate.queryForStream(sql, mapper).toList()
    }
}

Update

続いて、Update についてです。
Update は、Create と同様の処理を実装すれば完了します。

./src/main/kotlin/com/example/kotlingradlejdbcdemo/UpdateCommand.kt
package com.example.kotlingradlejdbcdemo

import org.springframework.jdbc.core.JdbcTemplate

interface UpdateCommand {
    fun perform(lastName: String)
}

class UpdateCommandImpl(val jdbcTemplate: JdbcTemplate) : UpdateCommand {
    val sql = """
        UPDATE
            customer
        SET
            first_name = 'Eve'
            , last_name = 'Sample4'
        WHERE
            last_name = ?
        ;
    """.trimIndent()

    override fun perform(lastName: String) {
        jdbcTemplate.update(sql, lastName)
    }
}

Delete

最後に Delete です。
Delete も Create、Update 同様の実装をすれば完了です。

./src/main/kotlin/com/example/kotlingradlejdbcdemo/DeleteCommand.kt
package com.example.kotlingradlejdbcdemo

import org.springframework.jdbc.core.JdbcTemplate

interface DeleteCommand {
    fun perform(firstName: String)
}

class DeleteCommandImpl(val jdbcTemplate: JdbcTemplate) : DeleteCommand {
    val sql = "DELETE FROM customer WHERE first_name = ?"
    override fun perform(firstName: String) {
        jdbcTemplate.update(sql, firstName)
    }
}

動作確認

では、実際に動作確認できているか確認します。
./src/main/kotlin/com/example/kotlingradlejdbcdemo/KotlinGradleJdbcDemoApplication.ktにクラスの実行を記述します。
以下のように記述してください。
jdbcTemplateは DI してくれるので、実行時に引数をインスタンスにしてくれます。

./src/main/kotlin/com/example/kotlingradlejdbcdemo/KotlinGradleJdbcDemoApplication.kt
package com.example.kotlingradlejdbcdemo

import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.jdbc.core.JdbcTemplate

@SpringBootApplication
class KotlinGradleJdbcDemoApplication(val jdbcTemplate: JdbcTemplate) : ApplicationRunner {
    override fun run(args: ApplicationArguments?) {
        println("--Create--")
        InsertCommandImpl(jdbcTemplate).perform("Carol", "Sample3")
        println("--Read--")
        SelectAllQueryImpl(jdbcTemplate).perform().forEach { println(it) }
        println("--Update--")
        UpdateCommandImpl(jdbcTemplate).perform("Sample3")
        println("--Read--")
        SelectAllQueryImpl(jdbcTemplate).perform().forEach { println(it) }
        println("--Delete--")
        DeleteCommandImpl(jdbcTemplate).perform("Eve")
        println("--Read--")
        SelectAllQueryImpl(jdbcTemplate).perform().forEach { println(it) }
    }
}

fun main(args: Array<String>) {
    runApplication<KotlinGradleJdbcDemoApplication>(*args)
}

では、動作確認します。
DB を起動してください。

DBの起動確認
# DB を起動
docker-compose up sample-pg pg-web
# DBを停止 & 削除
# docker-compose down

DB の起動を確認できたら、できたらアプリケーションを実行します。
Intellij の実行ボタンを押してもよいですが、ここでは ./gradlewを用いて CLI で起動します。
以下を実行してください。

実行コマンド
./gradlew bootRun

いろいろログがでてきます。
以下のように、Create、Read、Update、Delete、を確認できたら、成功です。

--Create--
2022-06-02 07:47:41.925  INFO 16013 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-06-02 07:47:42.098  INFO 16013 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
--Read--
Customer(id=1, firstName=Alice, lastName=Sample1)
Customer(id=2, firstName=Bob, lastName=Sample2)
Customer(id=3, firstName=Carol, lastName=Sample3)
--Update--
--Read--
Customer(id=1, firstName=Alice, lastName=Sample1)
Customer(id=2, firstName=Bob, lastName=Sample2)
Customer(id=3, firstName=Eve, lastName=Sample4)
--Delete--
--Read--
Customer(id=1, firstName=Alice, lastName=Sample1)
Customer(id=2, firstName=Bob, lastName=Sample2)
2022-06-02 07:47:42.279  INFO 16013 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-06-02 07:47:42.284  INFO 16013 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

まとめ

Kotlin、JDBC、PostgreSQL を用いた簡単な CRUD ができるアプリケーションを実装、紹介しました。
ここまでの内容を手を動かして写経するだけでも、Kotlin の型システムや、Intellij が持つ補完機能の強力さを感じられます。
ぜひ、Kotlin で簡単なことを実装してみたいけど悩んでいる方の助けになれば幸いです。
今回は実利用を完全に無視した内容ですので、次回は Web API の作成を考えています。

参考

https://spring.pleiades.io/guides/gs/relational-data-access/

https://qiita.com/suema0331/items/5062bcf008d507a58b75

Discussion