Skip to content

Spring Bootプロジェクトにデータベースのサポートを追加する

これはSpring BootとKotlin入門チュートリアルの第3部です。進む前に、前の手順を完了していることを確認してください。


First step KotlinでSpring Bootプロジェクトを作成する
Second step Spring Bootプロジェクトにデータクラスを追加する
Third step Spring Bootプロジェクトにデータベースのサポートを追加する
Fourth step データベースアクセスにSpring Data CrudRepositoryを使用する

このチュートリアルのパートでは、Java Database Connectivity (JDBC) を使用して、データベースをプロジェクトに追加し、設定します。 JVMアプリケーションでは、JDBCを使用してデータベースと対話します。 Spring Frameworkは、利便性のために JdbcTemplate クラスを提供しており、JDBCの使用を簡素化し、よくあるエラーを回避するのに役立ちます。

データベースサポートを追加する

Spring Frameworkベースのアプリケーションでは、データベースアクセスロジックをいわゆるサービス層 (service layer) —ビジネスロジックが存在する場所— の中に実装するのが一般的な慣習です。 Springでは、クラスがアプリケーションのサービス層に属することを意味するために、@Service アノテーションでクラスをマークする必要があります。 このアプリケーションでは、この目的のために MessageService クラスを作成します。

同じパッケージに MessageService.kt ファイルと MessageService クラスを次のように作成します。

kotlin
// MessageService.kt
package com.example.demo

import org.springframework.stereotype.Service
import org.springframework.jdbc.core.JdbcTemplate
import java.util.*

@Service
class MessageService(private val db: JdbcTemplate) {
    fun findMessages(): List<Message> = db.query("select * from messages") { response, _ ->
        Message(response.getString("id"), response.getString("text"))
    }

    fun save(message: Message): Message {
        db.update(
            "insert into messages values ( ?, ? )",
            message.id, message.text
        )
        return message
    }
}
コンストラクタ引数と依存性注入 – (private val db: JdbcTemplate)

Kotlinのクラスにはプライマリコンストラクタがあります。また、1つまたは複数のセカンダリコンストラクタを持つこともできます。 プライマリコンストラクタはクラスヘッダーの一部であり、クラス名とオプションの型パラメータの後に続きます。このケースでは、コンストラクタは (val db: JdbcTemplate) です。

val db: JdbcTemplate はコンストラクタの引数です。

kotlin
末尾ラムダとSAM変換

findMessages() 関数は JdbcTemplate クラスの query() 関数を呼び出します。query() 関数は2つの引数を取ります。1つはStringインスタンスとしてのSQLクエリ、もう1つは行ごとに1つのオブジェクトをマッピングするコールバックです。

sql

RowMapper インターフェースは1つのメソッドのみを宣言しているため、インターフェース名を省略してラムダ式で実装することが可能です。Kotlinコンパイラは、関数呼び出しのパラメータとして使用しているため、ラムダ式が変換されるべきインターフェースを認識しています。これはKotlinにおけるSAM変換として知られています。

sql

SAM変換の後、query関数は2つの引数、すなわち最初の位置にString、最後の位置にラムダ式を持つことになります。Kotlinの規約によれば、関数の最後のパラメータが関数である場合、対応する引数として渡されるラムダ式は括弧の外に配置できます。このような構文は末尾ラムダとも呼ばれます。

sql
未使用のラムダ引数に対するアンダースコア

複数のパラメータを持つラムダの場合、使用しないパラメータの名前をアンダースコア _ 文字に置き換えることができます。

したがって、query関数の呼び出しの最終的な構文は次のようになります。

kotlin

MessageControllerクラスを更新する

MessageController.kt を更新して、新しい MessageService クラスを使用するようにします。

kotlin
// MessageController.kt
package com.example.demo

import org.springframework.http.ResponseEntity
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.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.net.URI

@RestController
@RequestMapping("/")
class MessageController(private val service: MessageService) {
    @GetMapping
    fun listMessages() = service.findMessages()

    @PostMapping
    fun post(@RequestBody message: Message): ResponseEntity<Message> {
        val savedMessage = service.save(message)
        return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage)
    }
}
@PostMapping アノテーション

HTTP POSTリクエストを処理するメソッドには @PostMapping アノテーションを付与する必要があります。HTTP Bodyコンテンツとして送信されたJSONをオブジェクトに変換できるようにするには、メソッド引数に @RequestBody アノテーションを使用する必要があります。Jacksonライブラリがアプリケーションのクラスパスにあるおかげで、変換は自動的に行われます。

ResponseEntity

ResponseEntity は、ステータスコード、ヘッダー、ボディを含むHTTPレスポンス全体を表します。

created() メソッドを使用すると、レスポンスのステータスコード (201) を設定し、作成されたリソースのコンテキストパスを示すロケーションヘッダーを設定できます。

MessageServiceクラスを更新する

Message クラスの id はnull許容のStringとして宣言されていました。

kotlin
data class Message(val id: String?, val text: String)

しかし、データベースに nullid 値として保存するのは正しくありません。この状況を適切に処理する必要があります。

MessageService.kt ファイルのコードを更新し、メッセージをデータベースに保存する際に idnull の場合に新しい値を生成するようにします。

kotlin
// MessageService.kt
package com.example.demo

import org.springframework.stereotype.Service
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.query
import java.util.UUID

@Service
class MessageService(private val db: JdbcTemplate) {
    fun findMessages(): List<Message> = db.query("select * from messages") { response, _ ->
        Message(response.getString("id"), response.getString("text"))
    }

    fun save(message: Message): Message {
        val id = message.id ?: UUID.randomUUID().toString() // idがnullの場合に新しいidを生成する
        db.update(
            "insert into messages values ( ?, ? )",
            id, message.text
        )
        return message.copy(id = id) // 新しいidを持つメッセージのコピーを返す
    }
}
エルビス演算子 – ?:

コード message.id ?: UUID.randomUUID().toString() は、エルビス演算子 (if-not-null-else shorthand) ?: を使用しています。?: の左側の式が null でない場合、エルビス演算子はその値を返します。それ以外の場合は、右側の式を返します。右側の式は、左側が null の場合にのみ評価されることに注意してください。

アプリケーションコードはデータベースと連携する準備ができています。次に、データソースを設定する必要があります。

データベースを設定する

アプリケーションのデータベースを設定します。

  1. src/main/resources ディレクトリに schema.sql ファイルを作成します。このファイルにはデータベースオブジェクトの定義が格納されます。

    Create database schema

  2. src/main/resources/schema.sql ファイルを次のコードで更新します。

    sql
    -- schema.sql
    CREATE TABLE IF NOT EXISTS messages (
    id       VARCHAR(60)  PRIMARY KEY,
    text     VARCHAR      NOT NULL
    );

    これにより、idtext の2つのカラムを持つ messages テーブルが作成されます。このテーブル構造は Message クラスの構造と一致します。

  3. src/main/resources フォルダーにある application.properties ファイルを開き、次のアプリケーションプロパティを追加します。

    none
    spring.application.name=demo
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.url=jdbc:h2:file:./data/testdb
    spring.datasource.username=name
    spring.datasource.password=password
    spring.sql.init.schema-locations=classpath:schema.sql
    spring.sql.init.mode=always

    これらの設定は、Spring Bootアプリケーションのデータベースを有効にします。 一般的なアプリケーションプロパティの完全なリストは、Spring ドキュメントを参照してください。

HTTPリクエストを介してデータベースにメッセージを追加する

以前に作成したエンドポイントを操作するには、HTTPクライアントを使用する必要があります。IntelliJ IDEAでは、組み込みHTTPクライアントを使用します。

  1. アプリケーションを実行します。アプリケーションが起動して実行されたら、POSTリクエストを実行してメッセージをデータベースに保存できます。

  2. プロジェクトのルートフォルダに requests.http ファイルを作成し、次のHTTPリクエストを追加します。

    http
    ### Post "Hello!"
    POST http://localhost:8080/
    Content-Type: application/json
    
    {
      "text": "Hello!"
    }
    
    ### Post "Bonjour!"
    
    POST http://localhost:8080/
    Content-Type: application/json
    
    {
      "text": "Bonjour!"
    }
    
    ### Post "Privet!"
    
    POST http://localhost:8080/
    Content-Type: application/json
    
    {
      "text": "Privet!"
    }
    
    ### Get all the messages
    GET http://localhost:8080/
  3. すべてのPOSTリクエストを実行します。リクエスト宣言の横にあるガターの緑色の実行アイコンを使用します。 これらのリクエストは、テキストメッセージをデータベースに書き込みます。

    Execute POST request

  4. GETリクエストを実行し、Runツールウィンドウで結果を確認します。

    Execute GET requests

リクエストを実行する別の方法

他のHTTPクライアントやcURLコマンドラインツールを使用することもできます。たとえば、同じ結果を得るには、ターミナルで次のコマンドを実行します。

bash
curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Hello!\" }"

curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Bonjour!\" }"

curl -X POST --location "http://localhost:8080" -H "Content-Type: application/json" -d "{ \"text\": \"Privet!\" }"

curl -X GET --location "http://localhost:8080"

idでメッセージを取得する

idで個別のメッセージを取得する機能のアプリケーションを拡張します。

  1. MessageService クラスに、idで個別のメッセージを取得するための新しい関数 findMessageById(id: String) を追加します。

    kotlin
    // MessageService.kt
    package com.example.demo
    
    import org.springframework.stereotype.Service
    import org.springframework.jdbc.core.JdbcTemplate
    import org.springframework.jdbc.core.query
    import java.util.*
    
    @Service
    class MessageService(private val db: JdbcTemplate) {
        fun findMessages(): List<Message> = db.query("select * from messages") { response, _ ->
            Message(response.getString("id"), response.getString("text"))
        }
    
        fun findMessageById(id: String): Message? = db.query("select * from messages where id = ?", id) { response, _ ->
            Message(response.getString("id"), response.getString("text"))
        }.singleOrNull()
    
        fun save(message: Message): Message {
            val id = message.id ?: UUID.randomUUID().toString() // idがnullの場合に新しいidを生成する
            db.update(
                "insert into messages values ( ?, ? )",
                id, message.text
            )
            return message.copy(id = id) // 新しいidを持つメッセージのコピーを返す
        }
    }
    パラメータリストにおける可変引数 (vararg) の位置

    query() 関数は3つの引数を取ります。

    • 実行にパラメータを必要とするSQLクエリ文字列
    • String型のパラメータである id
    • ラムダ式で実装される RowMapper インスタンス

    query() 関数の2番目のパラメータは、可変引数 (vararg) として宣言されています。Kotlinでは、可変引数パラメータの位置はパラメータリストの最後である必要はありません。

    singleOrNull() 関数

    singleOrNull() 関数は、配列が空の場合、または同じ値を持つ要素が複数ある場合は null を返し、それ以外の場合は単一の要素を返します。

    .query() 関数は、メッセージをIDで取得するために使用され、Spring Frameworkによって提供されるKotlin拡張関数です。上記のコードで示されているように、追加のインポート import org.springframework.jdbc.core.query が必要です。

  2. MessageController クラスに、id パラメータを持つ新しい index(...) 関数を追加します。

    kotlin
    // MessageController.kt
    package com.example.demo
    
    import org.springframework.http.ResponseEntity
    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.RequestBody
    import org.springframework.web.bind.annotation.RequestMapping
    import org.springframework.web.bind.annotation.RestController
    import java.net.URI
    
    @RestController
    @RequestMapping("/")
    class MessageController(private val service: MessageService) {
        @GetMapping
        fun listMessages() = ResponseEntity.ok(service.findMessages())
        
        @PostMapping
        fun post(@RequestBody message: Message): ResponseEntity<Message> {
            val savedMessage = service.save(message)
            return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage)
        }
        
        @GetMapping("/{id}")
        fun getMessage(@PathVariable id: String): ResponseEntity<Message> =
            service.findMessageById(id).toResponseEntity()
        
        private fun Message?.toResponseEntity(): ResponseEntity<Message> =
            // メッセージがnull(見つからない)の場合、レスポンスコードを404に設定する
            this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() 
    }
    コンテキストパスからの値の取得

    メッセージの id は、新しい関数に @GetMapping("/{id}") アノテーションを付けることで、Spring Frameworkによってコンテキストパスから取得されます。関数引数に @PathVariable アノテーションを付けることで、取得した値を関数引数として使用するようにフレームワークに指示します。新しい関数は、そのidによって個別のメッセージを取得するために MessageService を呼び出します。

    null許容レシーバーを持つ拡張関数

    拡張は、null許容レシーバー型で定義できます。レシーバーが null の場合、thisnull になります。したがって、null許容レシーバー型で拡張を定義する場合、関数本体内で this == null チェックを実行することをお勧めします。

    また、上記の toResponseEntity() 関数のように、null安全な呼び出し演算子 (?.) を使用してnullチェックを実行することもできます。

    kotlin
    ResponseEntity

    ResponseEntity は、ステータスコード、ヘッダー、ボディを含むHTTPレスポンスを表します。これは、コンテンツをより細かく制御して、カスタマイズされたHTTPレスポンスをクライアントに送信できる汎用ラッパーです。

アプリケーションの完全なコードを以下に示します。

kotlin
// DemoApplication.kt
package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}
kotlin
// Message.kt
package com.example.demo

data class Message(val id: String?, val text: String)
kotlin
// MessageService.kt
package com.example.demo

import org.springframework.stereotype.Service
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.query
import java.util.*

@Service
class MessageService(private val db: JdbcTemplate) {
    fun findMessages(): List<Message> = db.query("select * from messages") { response, _ ->
        Message(response.getString("id"), response.getString("text"))
    }

    fun findMessageById(id: String): Message? = db.query("select * from messages where id = ?", id) { response, _ ->
        Message(response.getString("id"), response.getString("text"))
    }.singleOrNull()

    fun save(message: Message): Message {
        val id = message.id ?: UUID.randomUUID().toString()
        db.update(
            "insert into messages values ( ?, ? )",
            id, message.text
        )
        return message.copy(id = id)
    }
}
kotlin
// MessageController.kt
package com.example.demo

import org.springframework.http.ResponseEntity
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.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.net.URI

@RestController
@RequestMapping("/")
class MessageController(private val service: MessageService) {
    @GetMapping
    fun listMessages() = ResponseEntity.ok(service.findMessages())

    @PostMapping
    fun post(@RequestBody message: Message): ResponseEntity<Message> {
        val savedMessage = service.save(message)
        return ResponseEntity.created(URI("/${savedMessage.id}")).body(savedMessage)
    }

    @GetMapping("/{id}")
    fun getMessage(@PathVariable id: String): ResponseEntity<Message> =
        service.findMessageById(id).toResponseEntity()

    private fun Message?.toResponseEntity(): ResponseEntity<Message> =
        this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build()
}

アプリケーションを実行する

Springアプリケーションを実行する準備ができました。

  1. アプリケーションを再度実行します。

  2. requests.http ファイルを開き、新しいGETリクエストを追加します。

    http
    ### Get the message by its id
    GET http://localhost:8080/id
  3. GETリクエストを実行して、データベースからすべてのメッセージを取得します。

  4. RunツールウィンドウでいずれかのIDをコピーし、次のようにリクエストに追加します。

    http
    ### Get the message by its id
    GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707

    上記のメッセージIDの代わりに、あなたのメッセージIDを記述してください。

  5. GETリクエストを実行し、Runツールウィンドウで結果を確認します。

    Retrieve message by its id

次のステップ

最後のステップでは、Spring Dataを使用してより一般的なデータベース接続を行う方法を示します。

次の章に進む