Kotlin + Gradle + SpringWeb + JDBC でCRUD処理を実行するWeb APIサーバーを作成する
概要
前回、Kotlin と JDBC を用いて CRUD 処理を実行できるアプリケーションを作成しました。
実際に使うときには、API を使うことがほとんどですので、今回は API サーバを作成しました。
完成したソースコードは以下になります。
前提
プログラミング言語と DB は以下の構成で実装します。
Kotlin と PostgrSQL の基本的なこと、Intellij の使い方については知っている前提です。
項目 | 要素 |
---|---|
プログラミング言語 | Kotlin |
DB | PostgreSQL |
OS | macOS |
IDE | Intellij IDEA 2022.1.1(Apple Silicon 版) |
Spring Initializr
Spring Initializr を用いて、ひな型を作成します。
以下のリンクから、Spring Initializr の画面を開いてください。
設定は以下にします。
項目 | 要素 |
---|---|
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 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 を実行する関数になります。
package com.example.kotlingradlejdbcwebapidemo.repository
interface CustomerRepository {
fun add(firstName: String, lastName: String)
}
Repositoy には@Repository
アノテーションをつけます。
本記事では、@Component
と同じ挙動になりますが、わかりやすくなるため記述します。
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
クラスを作成します。
package com.example.kotlingradlejdbcwebapidemo.service
interface CustomerService {
fun insertCustomer(firstName: String, lastName: String)
}
Service には@Service
アノテーションをつけます。
Repository と同様に、本記事では@Component
と挙動に変わりはありません。
CustomerRepository
は、実行時に自動でCustomerRepositoryImpl
が DI されます(コンストラクタインジェクション)。
サービス層はトランザクションを管理するので、それぞれの関数に@Transactinal
アノテーションをつけていきます。
本記事では、実際にロールバックを管理する例はありませんが、一応つけます。
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 リクエストを受け付けます。
正常終了したことをレスポンスで確認するために、戻り値を記述しています。ここは、もっと良いやり方があると考えていますが、本記事ではこのやり方でやります。
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
を作成します。
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 のカラムと対応づけが可能です。
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 用のメソッドを追加します。
package com.example.kotlingradlejdbcwebapidemo.repository
import com.example.kotlingradlejdbcwebapidemo.model.Customer
interface CustomerRepository {
fun add(firstName: String, lastName: String)
fun find(): List<Customer
}
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 用のメソッドを追加します。
package com.example.kotlingradlejdbcwebapidemo.service
import com.example.kotlingradlejdbcwebapidemo.model.Customer
interface CustomerService {
fun insertCustomer(firstName: String, lastName: String)
fun selectCustomer(): List<Customer> // 追加
}
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 の変更をレスポンスが直接影響を受けないように定義します。
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 にメソッドを増やしましょう。
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 を実行します。
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 も同様の手順で実装していきます。
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) // 追加
}
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 が挿入されないようにしています。
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 にメソッドを増やすことから始めます。
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) // 追加
}
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 と同じ要領ですので、詳細な説明は省きます。
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) // 略
}
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 に渡すように実装すれば完了です。
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