Добавление поддержки баз данных в проект Spring Boot
В этой части руководства вы добавите и настроите базу данных в проекте с помощью Java Database Connectivity (JDBC). В приложениях для JVM для взаимодействия с базами данных используется JDBC. Для удобства Spring Framework предоставляет класс JdbcTemplate, который упрощает работу с JDBC и помогает избежать распространённых ошибок.
Добавление поддержки баз данных
Обычно в приложениях на основе 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
@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 есть первичный конструктор. Также у него может быть один или несколько вторичных конструкторов. Первичный конструктор — часть заголовка класса; он располагается после имени класса и необязательных параметров типа. В нашем случае конструктор —
(val db: JdbcTemplate).val db: JdbcTemplate— это аргумент конструктора:@Service class MessageService(private val db: JdbcTemplate) - Замыкающее лямбда-выражение и SAM-преобразование
-
Функция
findMessages()вызывает функциюquery()классаJdbcTemplate. Функцияquery()принимает два аргумента: SQL-запрос в виде экземпляра String и функцию обратного вызова, которая сопоставляет один объект каждой строке:db.query("...", RowMapper { ... } )Интерфейс
RowMapperобъявляет только один метод, поэтому его можно реализовать с помощью лямбда-выражения, не указывая имя интерфейса. Компилятор Kotlin знает, к какому интерфейсу нужно преобразовать лямбда-выражение, поскольку оно используется как параметр вызова функции. Это называется SAM-преобразованием в Kotlin:db.query("...", { ... } )После SAM-преобразования у функции запроса остаются два аргумента: String на первом месте и лямбда-выражение на последнем. Согласно соглашениям Kotlin, если последний параметр функции — это функция, то лямбда-выражение, переданное как соответствующий аргумент, можно поместить за скобками. Такой синтаксис также называется замыкающим лямбда-выражением:
db.query("...") { ... } - Подчёркивание для неиспользуемого аргумента лямбда-выражения
-
В лямбда-выражении с несколькими параметрами можно использовать символ подчёркивания
_вместо имён неиспользуемых параметров.Итоговый синтаксис вызова функции запроса выглядит так:
db.query("select * from messages") { response, _ -> Message(response.getString("id"), response.getString("text")) }
Обновление класса MessageController
Обновите MessageController.kt, чтобы использовать новый класс MessageService:
// 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. Чтобы преобразовать JSON, отправленный в теле HTTP-запроса, в объект, нужно использовать аннотацию@RequestBodyдля аргумента метода. Благодаря наличию библиотеки Jackson в classpath приложения преобразование выполняется автоматически.- ResponseEntity
-
ResponseEntityпредставляет HTTP-ответ целиком: код состояния, заголовки и тело.С помощью метода
created()можно настроить код состояния ответа (201) и задать заголовок location, указывающий путь в контексте для созданного ресурса.
Обновление класса MessageService
id для класса Message был объявлен как строка с возможным значением null:
data class Message(val id: String?, val text: String)
Однако сохранять null в базе данных как значение id неправильно: эту ситуацию нужно корректно обработать.
Обновите код файла MessageService.kt, чтобы при сохранении сообщений в базе данных генерировалось новое значение, если id имеет значение null:
// MessageService.kt
package com.example.demo
import org.springframework.stereotype.Service
import org.springframework.jdbc.core.JdbcTemplate
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()используется оператор Элвиса (сокращённая запись «если не null, иначе»):?:. Если выражение слева от?:не равноnull, оператор Элвиса возвращает его; в противном случае он возвращает выражение справа. Обратите внимание: выражение справа вычисляется только в том случае, если выражение слева имеет значениеnull.
Код приложения готов к работе с базой данных. Теперь необходимо настроить источник данных.
Настройка базы данных
Настройте базу данных в приложении:
-
Создайте файл
schema.sqlв каталогеsrc/main/resources. В нём будут храниться определения объектов базы данных: -
Обновите файл
src/main/resources/schema.sql, добавив следующий код:-- schema.sql CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(60) PRIMARY KEY, text VARCHAR NOT NULL );
Он создаёт таблицу
messagesс двумя столбцами:idиtext. Структура таблицы соответствует структуре классаMessage. -
Откройте файл
application.propertiesв папкеsrc/main/resourcesи добавьте следующие свойства приложения: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-клиентом:
Запустите приложение. После его запуска можно выполнять POST-запросы для сохранения сообщений в базе данных.
-
Создайте файл
requests.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-запросы. Нажмите зелёный значок Запустить на поле рядом с объявлением запроса. Эти запросы записывают текстовые сообщения в базу данных:

-
Выполните GET-запрос и просмотрите результат в окне инструментов Запустить:

Другой способ выполнения запросов
Можно также использовать любой другой 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"
Получение сообщений по идентификатору
Расширьте функциональность приложения, чтобы получать отдельные сообщения по идентификатору.
-
Добавьте в класс
MessageServiceновую функциюfindMessageById(id: 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() // 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()принимает три аргумента:Строка SQL-запроса, для выполнения которого требуется параметр
id— параметр типа StringЭкземпляр
RowMapper, реализуемый с помощью лямбда-выражения
Второй параметр функции
query()объявлен как параметр переменного числа аргументов (vararg). В Kotlin параметр переменного числа аргументов не обязательно должен быть последним в списке параметров. - Функция singleOrNull()
Функция
singleOrNull()возвращает единственный элемент илиnull, если массив пуст или содержит более одного элемента с одинаковым значением.
-
Добавьте в класс
MessageControllerновую функциюindex(...)с параметром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> = // If the message is null (not found), set response code to 404 this?.let { ResponseEntity.ok(it) } ?: ResponseEntity.notFound().build() }- Получение значения из пути в контексте
Сообщение
idизвлекается Spring Framework из пути в контексте, поскольку новая функция помечена аннотацией@GetMapping("/{id}"). Пометив аргумент функции аннотацией@PathVariable, вы указываете фреймворку использовать извлечённое значение в качестве аргумента функции. Новая функция вызываетMessageServiceдля получения отдельного сообщения по его идентификатору.- Функция расширения с nullable-приёмником
-
Функции расширения можно определять с типом приёмника, допускающим значение null. Если приёмник равен
null, тоthisтакже равноnull. Поэтому при определении расширения с типом приёмника, допускающим значение null, рекомендуется выполнять проверкуthis == nullв теле функции.Для проверки на null также можно использовать оператор безопасного вызова (
?.), как в приведённой выше функцииtoResponseEntity():this?.let { ResponseEntity.ok(it) } - ResponseEntity
ResponseEntityпредставляет HTTP-ответ, включая код состояния, заголовки и тело. Это универсальная оболочка, которая позволяет отправлять клиенту настраиваемые HTTP-ответы и более гибко управлять их содержимым.
Полный код приложения:
Запуск приложения
Приложение Spring готово к запуску:
Запустите приложение ещё раз.
-
Откройте файл
requests.httpи добавьте новый GET-запрос:### Get the message by its id GET http://localhost:8080/id
Выполните GET-запрос, чтобы получить все сообщения из базы данных.
-
В окне инструментов Запустить скопируйте один из идентификаторов и добавьте его в запрос, как показано ниже:
### Get the message by its id GET http://localhost:8080/f910aa7e-11ee-4215-93ed-1aeeac822707
-
Выполните GET-запрос и просмотрите результат в окне инструментов Запустить:

Следующий шаг
На последнем шаге вы узнаете, как использовать более популярный способ подключения к базе данных с помощью Spring Data.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/jvm-spring-boot-add-db-support.html