🫖

Kotlin + Gradle + SpringWeb + JDBC でCRUD処理を実行するWeb APIサーバーを作成する

2022/06/10に公開

概要

前回、Kotlin と JDBC を用いて CRUD 処理を実行できるアプリケーションを作成しました。

https://zenn.dev/msksgm/articles/20220603-kotlin-jdbc-postgresql

実際に使うときには、API を使うことがほとんどですので、今回は API サーバを作成しました。

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

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

前提

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

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

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-web-api-demo
Project Metadata Name kotlin-gradle-jdbc-web-api-demo
Packaging Jar
Java 17
Dependencies PostgreSQL Driver、JDBC API、Spring Web

spring_initializer
Spring Initializr の設定画面

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

5 directories, 5 files

DB

JDBC を使用するときには、起動した時点で DB と接続できないと、アプリケーションが落ちてしまいます。
そのため、あらかじめ、application.properties と接続先の DB の設定を済ませておきます。
実装方法は前回と同様なので省略します。
前回記事の DB の箇所Github のリポジトリを参考にしてください。

実装

これから、Kotlin、SpringBoot、JDBC で CRUD ができる Web API を実装します。
アーキテクチャは、さまざまな形式が考えられます。
本記事では、以下の表のような 4 つの簡単な構成で作成します。
以降、./src/main/kotlin/com/example/kotlingradlejdbcwebapidemo/配下にそれぞれの package を記述していきます。
特に明記されていない場合は、./src/main/kotlin/com/example/kotlingradlejdbcwebapidemo/が省略されていると考えてください。

package 名 役割
model データモデルを定義
repository RDB との接続、SQL の実行を定義
service controller と repository の処理をつなぎ、トランザクションを定義
controller Rest API のルーティングを定義

Create

まず、Create からできるようにします。
最初、PostgreSQL との接続と SQL を発行する Repository を作成します。
repositoryパッケージを作成し、パッケージ配下に記述します。
CustomerRepositoryインタフェースとCustomerRepositoryImplクラスを作成します。
メソッッドaddが DB に対して INSERT を実行する関数になります。

repository/CustomerRepository.kt
package com.example.kotlingradlejdbcwebapidemo.repository

interface CustomerRepository {
    fun add(firstName: String, lastName: String)
}

Repositoy には@Repositoryアノテーションをつけます。
本記事では、@Componentと同じ挙動になりますが、わかりやすくなるため記述します。

repository/CustomerRepositoryImpl.kt
package com.example.kotlingradlejdbcwebapidemo.repository

import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository

@Repository
class CustomerRepositoryImpl(val jdbcTemplate: JdbcTemplate) : CustomerRepository {
    override fun add(firstName: String, lastName: String) {
        val sql = "INSERT INTO customer(first_name, last_name) VALUES (?, ?);"
        jdbcTemplate.update(sql, firstName, lastName)
        return
    }
}

続いて、Service の実装です。
serviceを作成しパッケージ配下に記述します。
CustomerServiceインタフェースとCustomerServiceImplクラスを作成します。

service/CustomerService.kt
package com.example.kotlingradlejdbcwebapidemo.service

interface CustomerService {
    fun insertCustomer(firstName: String, lastName: String)
}

Service には@Serviceアノテーションをつけます。
Repository と同様に、本記事では@Componentと挙動に変わりはありません。
CustomerRepositoryは、実行時に自動でCustomerRepositoryImplが DI されます(コンストラクタインジェクション)。
サービス層はトランザクションを管理するので、それぞれの関数に@Transactinalアノテーションをつけていきます。
本記事では、実際にロールバックを管理する例はありませんが、一応つけます。

service/CustomerServiceImpl.kt
package com.example.kotlingradlejdbcwebapidemo.service

import com.example.kotlingradlejdbcwebapidemo.repository.CustomerRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class CustomerServiceImpl(val customerRepository: CustomerRepository) : CustomerService {
    @Transactional
    override fun insertCustomer(firstName: String, lastName: String) {
        customerRepository.add(firstName, lastName)
        return
    }
}

最後に、Controller を作成します。
ほかのパッケージと同様に、controllerパッケージを作成します。
controllerパッケージを作成し、ルーティングの設定を記述します。
@RestContollerで RestAPI のコントローラであることを宣言し、PostMappingで該当するパスの POST リクエストを受け付けます。
正常終了したことをレスポンスで確認するために、戻り値を記述しています。ここは、もっと良いやり方があると考えていますが、本記事ではこのやり方でやります。

controller/CustomerController.kt
package com.example.kotlingradlejdbcwebapidemo.controller

import com.example.kotlingradlejdbcwebapidemo.service.CustomerService
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
@Suppress("unused")
class CustomerController(val customerService: CustomerService) {
    @PostMapping("/customers")
    fun insert(@RequestBody request: CustomerInsertRequest): String {
        customerService.insertCustomer(request.firstName, request.lastName)
        return """
            {
                "message": "success"
            }
        """.trimIndent()
    }
}

簡単に、動作確認をします。DB を起動(docker compose up)してください。
コマンドラインで以下のコマンドを実行するか、Intellij の実行ボタンを押してください。

./gradlew bootRun

続いて、curl コマンドで以下を実行します。

curl --location --request POST 'http://localhost:8080/customers' \
--header 'Content-Type: application/json' \
--data-raw '{
    "first_name": "Carol",
    "last_name": "Sample3"
}'

レスポンスが正常に返ってきたら成功です。

{
  "message": "success"
}

Read

最初に、RDB からマッピング先のデータモデルを作成します。
modelパッケージを作成し、データクラスCustomerを作成します。

model/Customer.kt
package com.example.kotlingradlejdbcwebapidemo.model

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

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

repository/CustomerRowmapper.kt
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"),
        )
    }
}

そして、Repository に Read 用のメソッドを追加します。

repository/CustomerRepository.kt
package com.example.kotlingradlejdbcwebapidemo.repository

import com.example.kotlingradlejdbcwebapidemo.model.Customer

interface CustomerRepository {
    fun add(firstName: String, lastName: String)
    fun find(): List<Customer
}

repository/CustomerRepositoryImpl.kt
package com.example.kotlingradlejdbcwebapidemo.repository

import com.example.kotlingradlejdbcwebapidemo.model.Customer
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository

@Repository
class CustomerRepositoryImpl(val jdbcTemplate: JdbcTemplate) : CustomerRepository {
    override fun add(firstName: String, lastName: String) {
        // 略
    }

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

続いて、Service にも Read 用のメソッドを追加します。

service/CustomerService.kt
package com.example.kotlingradlejdbcwebapidemo.service

import com.example.kotlingradlejdbcwebapidemo.model.Customer

interface CustomerService {
    fun insertCustomer(firstName: String, lastName: String)
    fun selectCustomer(): List<Customer> // 追加
}

service/CustomerServiceImpl.kt
package com.example.kotlingradlejdbcwebapidemo.service

import com.example.kotlingradlejdbcwebapidemo.model.Customer
import com.example.kotlingradlejdbcwebapidemo.repository.CustomerRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class CustomerServiceImpl(val customerRepository: CustomerRepository) : CustomerService {
    @Transactional
    override fun insertCustomer(firstName: String, lastName: String) {
        // 略
    }

    @Transactional
    override fun selectCustomer(): List<Customer> {
        return customerRepository.find() // 追加
    }
}

同じ要領で、Controller にもメソッドを追加します。
その前に、レスポンスのデータクラス CustomerResponseを定義します。
実装としては、List<Customer>を詰め替えているだけですが、model の変更をレスポンスが直接影響を受けないように定義します。

controller/CustomerResponse.kt
package com.example.kotlingradlejdbcwebapidemo.controller

import com.example.kotlingradlejdbcwebapidemo.model.Customer

data class CustomerResponse(
    val customers: List<Customer>
)


そして、@GetMapping("/customers")を追加して、get メソッドを受け取るようにします。

package com.example.kotlingradlejdbcwebapidemo.controller

import com.example.kotlingradlejdbcwebapidemo.service.CustomerService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
@Suppress("unused")
class CustomerController(val customerService: CustomerService) {
    @PostMapping("/customers")
    fun insert(@RequestBody request: CustomerInsertRequest): String {
        // 略
    }

    @GetMapping("/customers")
    fun read(): CustomerResponse {
        return CustomerResponse(customers = customerService.selectCustomer())
    }
}

動作確認をします(DB は前回の状態を引き継いでいる前提)。
curl コマンドで以下を実行します。

curl --location --request GET 'http://localhost:8080/customers' | jq

以下の JSON が返ってきたら成功です。
Create の実装で追加されたオブジェクトが挿入されていることもわかります。

{
  "customers": [
    {
      "id": 1,
      "firstName": "Alice",
      "lastName": "Sample1"
    },
    {
      "id": 2,
      "firstName": "Bob",
      "lastName": "Sample2"
    },
    {
      "id": 3,
      "firstName": "Carol",
      "lastName": "Sample3"
    }
  ]
}

Update

続いて、Update を実装します。
まず、Repository にメソッドを増やしましょう。

repository/CustomerRepository.kt
package com.example.kotlingradlejdbcwebapidemo.repository

import com.example.kotlingradlejdbcwebapidemo.model.Customer

interface CustomerRepository {
    fun add(firstName: String, lastName: String)
    fun find(): List<Customer>
    fun update(id: Int, firstName: String, lastName: String) // 追加
}

基本的には Create と同じ要領でメソッドを追加します。
jdbcTempalte.updateで副作用のある SQL を実行します。

repository/CustomerRepositoryImpl.kt
package com.example.kotlingradlejdbcwebapidemo.repository

import com.example.kotlingradlejdbcwebapidemo.model.Customer
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository

@Repository
class CustomerRepositoryImpl(val jdbcTemplate: JdbcTemplate) : CustomerRepository {
    override fun add(firstName: String, lastName: String) {
        // 略
    }

    override fun find(): List<Customer> {
        // 略
    }

    override fun update(id: Int, firstName: String, lastName: String) {
        val sql = """
            UPDATE
                customer
            SET
                first_name = ?
                , last_name = ?
            WHERE
                id = ?
        """.trimIndent()
        jdbcTemplate.update(sql, firstName, lastName, id)
        return
    }
}

Service も同様の手順で実装していきます。

service/CustomerService.kt
package com.example.kotlingradlejdbcwebapidemo.service

import com.example.kotlingradlejdbcwebapidemo.model.Customer

interface CustomerService {
    fun insertCustomer(firstName: String, lastName: String)
    fun selectCustomer(): List<Customer>
    fun updateCustomer(id: Int, firstName: String, lastName: String) // 追加
}

service/CustomerServiceImpl.kt
package com.example.kotlingradlejdbcwebapidemo.service

import com.example.kotlingradlejdbcwebapidemo.model.Customer
import com.example.kotlingradlejdbcwebapidemo.repository.CustomerRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class CustomerServiceImpl(val customerRepository: CustomerRepository) : CustomerService {
    @Transactional
    override fun insertCustomer(firstName: String, lastName: String) {
        // 略
    }

    @Transactional
    override fun selectCustomer(): List<Customer> {
        // 略
    }

    @Transactional
    override fun updateCustomer(id: Int, firstName: String, lastName: String) {
        customerRepository.update(id, firstName, lastName)
        return
    }
}

最後に、Controller にメソッドを追加します。
その前に、今回の実装では PUT メソッドにリクエストボディを付与します。
そのためのデータクラスUpdateCustomerRequestを用意します。
リクエストボディと Kotlin のデータクラスをjacksonによって紐づけます。
今回は簡略化のため、デフォルト値を用意することで NULL が挿入されないようにしています。

controller/UpdateCustomerRequest.kt
package com.example.kotlingradlejdbcwebapidemo.controller

import com.fasterxml.jackson.annotation.JsonProperty

data class UpdateCustomerRequest(
    @JsonProperty("first_name") val firstName: String = "Eve-default",
    @JsonProperty("last_name") val lastName: String = "Sample4-default",
)

Contoller にメソッドを追加します。
パスパラメータは@PathVariable("id")で取得できます。

package com.example.kotlingradlejdbcwebapidemo.controller

import com.example.kotlingradlejdbcwebapidemo.service.CustomerService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
@Suppress("unused")
class CustomerController(val customerService: CustomerService) {
    @PostMapping("/customers")
    fun insert(@RequestBody request: CustomerInsertRequest): String {
        // 略
    }

    @GetMapping("/customers")
    fun read(): CustomerResponse {
        // 略
    }

    @PutMapping("/customers/{id}")
    fun update(@PathVariable("id") id: Int, @RequestBody request: UpdateCustomerRequest): String {
        customerService.updateCustomer(id, request.firstName, request.lastName)
        return """
            {
                "message": "success"
            }
        """.trimIndent()
    }
}

動作確認します。

以下のコマンドで PUT メソッドを実行します。

curl --location --request PUT 'http://localhost:8080/customers/3' \
--header 'Content-Type: application/json' \
--data-raw '{
    "first_name": "Eve",
    "last_name": "Sample4"
}'

以下のレスポンスがきたら成功です。

{
  "message": "success"
}

確認のため、Read します。

curl --location --request GET 'http://localhost:8080/customers'

以下のように更新が確認できます。

{
    "customers": [
        {
            "id": 1,
            "firstName": "Alice",
            "lastName": "Sample1"
        },
        {
            "id": 2,
            "firstName": "Bob",
            "lastName": "Sample2"
        },
        {
            "id": 3,
            "firstName": "Eve",
            "lastName": "Sample4"
        }
    ]
}

Delete

最後に Delete メソッドを実装します。
今までと同様に、Repository にメソッドを増やすことから始めます。

repository/CustomerRepository.kt
package com.example.kotlingradlejdbcwebapidemo.repository

import com.example.kotlingradlejdbcwebapidemo.model.Customer

interface CustomerRepository {
    fun add(firstName: String, lastName: String)
    fun find(): List<Customer>
    fun update(id: Int, firstName: String, lastName: String)
    fun delete(id: Int) // 追加
}

repository/CustomerRepositoryImpl.kt
package com.example.kotlingradlejdbcwebapidemo.repository

import com.example.kotlingradlejdbcwebapidemo.model.Customer
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository

@Repository
class CustomerRepositoryImpl(val jdbcTemplate: JdbcTemplate) : CustomerRepository {
    override fun add(firstName: String, lastName: String) {
        // 略
    }

    override fun find(): List<Customer> {
        // 略
    }

    override fun update(id: Int, firstName: String, lastName: String) {
        // 略
    }

    override fun delete(id: Int) {
        val sql = "DELETE FROM customer WHERE id = ?"
        jdbcTemplate.update(sql, id)
        return
    }
}

Service、Controller も両方とも Update と同じ要領ですので、詳細な説明は省きます。

service/CustomerService.kt
package com.example.kotlingradlejdbcwebapidemo.service

import com.example.kotlingradlejdbcwebapidemo.model.Customer

interface CustomerService {
    fun insertCustomer(firstName: String, lastName: String)
    fun selectCustomer(): List<Customer>
    fun updateCustomer(id: Int, firstName: String, lastName: String)
    fun deleteCustomer(id: Int) // 略
}

service/CustomerServiceImpl.kt
package com.example.kotlingradlejdbcwebapidemo.service

import com.example.kotlingradlejdbcwebapidemo.model.Customer
import com.example.kotlingradlejdbcwebapidemo.repository.CustomerRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class CustomerServiceImpl(val customerRepository: CustomerRepository) : CustomerService {
    @Transactional
    override fun insertCustomer(firstName: String, lastName: String) {
        // 略
    }

    @Transactional
    override fun selectCustomer(): List<Customer> {
        // 略
    }

    @Transactional
    override fun updateCustomer(id: Int, firstName: String, lastName: String) {
        // 略
    }

    @Transactional
    override fun deleteCustomer(id: Int) {
        customerRepository.delete(id)
        return
    }
}

@PathVariable("id")でパスパラメータを受け取って、Service に渡すように実装すれば完了です。

constoller/CusomerController.kt
package com.example.kotlingradlejdbcwebapidemo.controller

import com.example.kotlingradlejdbcwebapidemo.service.CustomerService
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
@Suppress("unused")
class CustomerController(val customerService: CustomerService) {
    @PostMapping("/customers")
    fun insert(@RequestBody request: CustomerInsertRequest): String {
        // 略
    }

    @GetMapping("/customers")
    fun read(): CustomerResponse {
        // 略
    }

    @PutMapping("/customers/{id}")
    fun update(@PathVariable("id") id: Int, @RequestBody request: UpdateCustomerRequest): String {
        // 略
    }

    @DeleteMapping("/customers/{id}")
    fun delete(@PathVariable("id") id: Int) : String {
        customerService.deleteCustomer(id)
        return """
            {
                "message": "success"
            }
        """.trimIndent()
    }
}

では、動作確認をします。
以下の curl コマンドを実行して、レスポンスが返ってくれば完了です。

curl --location --request DELETE 'http://localhost:8080/customers/3'
{
    "message": "success"
}

実行結果を確認してみます。
id=3が削除されたことを確認できました。

curl --location --request GET 'http://localhost:8080/customers'
{
    "customers": [
        {
            "id": 1,
            "firstName": "Alice",
            "lastName": "Sample1"
        },
        {
            "id": 2,
            "firstName": "Bob",
            "lastName": "Sample2"
        }
    ]
}

まとめ

Kotlin+SpringBoot+JDBC で Web API サーバを作成しました。
model、repository、service、controller に分けて実装しました。
実利用するにはより多くのことを考えなければいけませんが、とりあえず動く分には Kotlin でも簡単に作成できます。
これから、ライブラリ(Spring、JDBC、Jackson)周りについて調査していきます。

Discussion