Skip to content

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

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


最初のステップ Spring BootプロジェクトをKotlinで作成する
2番目のステップ Spring Bootプロジェクトにデータクラスを追加する
3番目のステップ Spring Bootプロジェクトにデータベースサポートを追加する
4番目のステップ Spring Data CrudRepositoryを使用したデータベースアクセス

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

データベースサポートの追加

Spring Frameworkベースのアプリケーションにおける一般的なプラクティスは、いわゆる_サービス_層(ビジネスロジックが存在する場所)内にデータベースアクセスロジックを実装することです。 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
@Service
class MessageService(private val db: JdbcTemplate)
末尾ラムダとSAM変換

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

sql
db.query("...", RowMapper { ... } )

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

sql
db.query("...", { ... } )

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

sql
db.query("...") { ... }
未使用のラムダ引数にアンダースコアを使用

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

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

kotlin
db.query("select * from messages") { response, _ ->
Message(response.getString("id"), response.getString("text"))
}

MessageControllerクラスの更新

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

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ボディの内容として送信されたJSONをオブジェクトに変換できるようにするには、メソッド引数に@RequestBodyアノテーションを使用する必要があります。アプリケーションのクラスパスにJacksonライブラリがあるため、変換は自動的に行われます。

ResponseEntity

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

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

MessageServiceクラスの更新

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

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

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

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

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() // Generate new id if it is null
        db.update(
            "insert into messages values ( ?, ? )",
            id, message.text
        )
        return message.copy(id = id) // Return a copy of the message with the new id
    }
}
エルビス演算子 – ?:

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

アプリケーションコードはデータベースと連携する準備ができました。次にデータソースを構成する必要があります。

データベースの構成

アプリケーションでデータベースを構成します。

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

    データベーススキーマの作成

  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リクエストを実行します。リクエスト宣言の隣にあるガターの緑色のRunアイコンを使用します。 これらのリクエストは、テキストメッセージをデータベースに書き込みます。

    POSTリクエストの実行

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

    GETリクエストの実行

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

他の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() // Generate new id if it is null
            db.update(
                "insert into messages values ( ?, ? )",
                id, message.text
            )
            return message.copy(id = id) // Return a copy of the message with the new id
        }
    }
    パラメータリストにおける可変長引数(vararg)の位置

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

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

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

    singleOrNull() 関数

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

    DANGER

    メッセージをIDで取得するために使用される.query()関数は、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> =
            // If the message is null (not found), set response code to 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
    this?.let { ResponseEntity.ok(it) }
    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

    NOTE

    上記のメッセージIDの代わりに、ご自身のメッセージIDを入力してください。

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

    IDでメッセージを取得

次のステップ

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

次の章に進む