Spring Bootプロジェクトにデータベースサポートを追加する
これは「**Spring BootとKotlin入門**」チュートリアルの第3部です。進む前に、以前のステップを完了していることを確認してください。
Spring BootプロジェクトをKotlinで作成する
Spring Bootプロジェクトにデータクラスを追加する
Spring Bootプロジェクトにデータベースサポートを追加する
Spring Data CrudRepositoryを使用したデータベースアクセス
このチュートリアルのパートでは、Java Database Connectivity (JDBC) を使用して、プロジェクトにデータベースを追加し、構成します。 JVMアプリケーションでは、JDBCを使用してデータベースと対話します。 利便性のため、Spring FrameworkはJDBCの使用を簡素化し、一般的なエラーを回避するのに役立つJdbcTemplate
クラスを提供しています。
データベースサポートの追加
Spring Frameworkベースのアプリケーションにおける一般的なプラクティスは、いわゆる_サービス_層(ビジネスロジックが存在する場所)内にデータベースアクセスロジックを実装することです。 Springでは、クラスがアプリケーションのサービス層に属することを示すために、@Service
アノテーションでクラスをマークする必要があります。 このアプリケーションでは、この目的のためにMessageService
クラスを作成します。
同じパッケージ内に、MessageService.kt
ファイルとMessageService
クラスを次のように作成します。
// 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
はコンストラクタの引数です。
@Service
class MessageService(private val db: JdbcTemplate)
末尾ラムダとSAM変換
findMessages()
関数は、JdbcTemplate
クラスのquery()
関数を呼び出します。query()
関数は2つの引数を取ります。1つはStringインスタンスとしてのSQLクエリ、もう1つは行ごとに1つのオブジェクトをマップするコールバックです。
db.query("...", RowMapper { ... } )
RowMapper
インターフェースは1つのメソッドしか宣言していないため、インターフェース名を省略してラムダ式で実装することができます。Kotlinコンパイラは、関数呼び出しのパラメータとして使用されているため、ラムダ式をどのインターフェースに変換する必要があるかを知っています。これはKotlinのSAM変換として知られています。
db.query("...", { ... } )
SAM変換後、query関数は、最初の位置にString、最後の位置にラムダ式の2つの引数で終わります。Kotlinの規約に従って、関数の最後のパラメータが関数の場合、対応する引数として渡されるラムダ式は括弧の外に配置できます。このような構文は末尾ラムダとしても知られています。
db.query("...") { ... }
未使用のラムダ引数にアンダースコアを使用
複数のパラメータを持つラムダの場合、使用しないパラメータの名前をアンダースコア_
文字で置き換えることができます。
したがって、query関数呼び出しの最終的な構文は次のようになります。
db.query("select * from messages") { response, _ ->
Message(response.getString("id"), response.getString("text"))
}
MessageControllerクラスの更新
新しいMessageService
クラスを使用するようにMessageController.kt
を更新します。
// 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として宣言されていました。
data class Message(val id: String?, val text: String)
しかし、null
をデータベースにid
値として格納するのは正しくありません。この状況を適切に処理する必要があります。
メッセージをデータベースに格納する際に、id
がnull
の場合に新しい値を生成するようにMessageService.kt
ファイルのコードを更新します。
// 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
の場合にのみ評価されることに注意してください。
アプリケーションコードはデータベースと連携する準備ができました。次にデータソースを構成する必要があります。
データベースの構成
アプリケーションでデータベースを構成します。
src/main/resources
ディレクトリにschema.sql
ファイルを作成します。これはデータベースオブジェクト定義を保存します。src/main/resources/schema.sql
ファイルを次のコードで更新します。sql-- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL );
これにより、
id
とtext
の2つのカラムを持つmessages
テーブルが作成されます。テーブル構造はMessage
クラスの構造と一致します。src/main/resources
フォルダにあるapplication.properties
ファイルを開き、以下のアプリケーションプロパティを追加します。nonespring.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クライアントを使用します。
アプリケーションを実行します。アプリケーションが起動して実行されると、POSTリクエストを実行してメッセージをデータベースに格納できます。
プロジェクトのルートフォルダに
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/
すべてのPOSTリクエストを実行します。リクエスト宣言の隣にあるガターの緑色のRunアイコンを使用します。 これらのリクエストは、テキストメッセージをデータベースに書き込みます。
GETリクエストを実行し、Runツールウィンドウで結果を確認します。
リクエストを実行する別の方法
他のHTTPクライアントやcURLコマンドラインツールを使用することもできます。たとえば、ターミナルで次のコマンドを実行すると、同じ結果が得られます。
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で個別のメッセージを取得するために、アプリケーションの機能を拡張します。
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 } }
- パラメータを必要とするSQLクエリ文字列
- String型のパラメータである
id
- ラムダ式によって実装された
RowMapper
インスタンス
パラメータリストにおける可変長引数(vararg)の位置
query()
関数は3つの引数を取ります。query()
関数の2番目のパラメータは、可変長引数(vararg
)として宣言されています。Kotlinでは、可変長引数パラメータの位置は、パラメータリストの最後にある必要はありません。singleOrNull() 関数
singleOrNull()
関数は、単一の要素を返します。配列が空であるか、または同じ値を持つ要素が複数ある場合はnull
を返します。DANGER
メッセージをIDで取得するために使用される
.query()
関数は、Spring Frameworkによって提供されるKotlin拡張関数です。上記のコードで示されているように、追加のインポートimport org.springframework.jdbc.core.query
が必要です。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
の場合、this
もnull
になります。したがって、null許容レシーバー型を持つ拡張関数を定義する際には、関数本体内でthis == null
チェックを実行することをお勧めします。上記の
toResponseEntity()
関数のように、null安全呼び出し演算子(?.
)を使用してnullチェックを実行することもできます。kotlinthis?.let { ResponseEntity.ok(it) }
ResponseEntity
ResponseEntity
は、ステータスコード、ヘッダ、ボディを含むHTTPレスポンスを表します。これは、より詳細なコンテンツの制御を可能にする汎用ラッパーであり、クライアントにカスタマイズされたHTTPレスポンスを送信できます。
アプリケーションの完全なコードを以下に示します。
// 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)
}
// Message.kt
package com.example.demo
data class Message(val id: String?, val text: String)
// 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)
}
}
// 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アプリケーションを実行する準備ができました。
アプリケーションを再度実行します。
requests.http
ファイルを開き、新しいGETリクエストを追加します。http### Get the message by its id GET http://localhost:8080/id
GETリクエストを実行して、データベースからすべてのメッセージを取得します。
RunツールウィンドウでいずれかのIDをコピーし、次のようにリクエストに追加します。
http### Get the message by its id GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707
NOTE
上記のメッセージIDの代わりに、ご自身のメッセージIDを入力してください。
GETリクエストを実行し、Runツールウィンドウで結果を確認します。
次のステップ
最後のステップでは、Spring Dataを使用して、より一般的なデータベース接続方法を紹介します。