Статьи Утилиты Telegram YouTube VK Видео RuTube Отзывы

Работа с БД в Spring Boot на примере postgresql

Исходники

8 марта 2026

Тэги: Java, PostgreSQL, rest, Spring Boot, SQL, Stream API, руководство.

Содержание

  1. Создание и заполнение таблицы
  2. Добавляем зависимость JDBC API
  3. Параметры подключения
  4. Слой взаимодействия с БД
  5. Новый сервисный слой
  6. Итоги

Данная статья является продолжением Spring Boot Restful Service, где была бы раскрыта тема работы с БД в Spring Boot. Давайте рассмотрим эту тему подробнее на примере СУБД postgresql, а в качестве основы возьмём проект, который мы делали в той статье.

Напомню, что проект представляет из себя простой restful-service, который принимает GET-запрос по HTTP и возвращает профиль пользователя по его id.

Создание и заполнение таблицы

Сам профиль содержит кроме id также имя, фамилию и возраст. Поэтому создадим таблицу profile в базе данных.

create table profile
(
    id         serial,
    first_name character varying(50) not null,
    last_name  character varying(50) not null,
    age        integer               not null,
    constraint profile_id_pk primary key (id)
);

insert into profile (first_name, last_name, age)
values ('Иван', 'Петров', 23);

Для поля id можно использовать тип serial. Он представляет собой целое число, которое увеличивается на 1 автоматически при вставке новой записи в таблицу.

Добавляем зависимость JDBC API

Для выполнения запросов в БД нам нужно добавить в наш проект ещё две зависимости:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

spring-boot-starter-jdbc позволяет выполнять sql-запросы в базу, а postgresql - это драйвер для работы с соответствующей СУБД. Обратите внимание, что он имеет runtime scope. То есть зависимость не нужна для компиляции, а только для работы приложения.

Параметры подключения

Для интеграции с БД также требуется указать параметры подключения, такие как логин, пароль и т.п. В файл application.yaml в папке resources добавим такое содержимое:

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/example_db
    username: example_user
    password: "!QAZxsw2"

Обратите внимание, что достаточно лишь прописать параметры подключения - и всё остальное Spring Boot сделает за вас. Например, инициализирует пул подключений.

Слой взаимодействия с БД

Для работы с БД принято выделять отдельный слой репозитория. Назовём его ProfileRepository:

@Repository
@RequiredArgsConstructor
public class ProfileRepository {

    private static final String SQL_GET_PROFILE_BY_ID =
            "select id, first_name, last_name, age from profile where id = :id";

    private final JdbcClient jdbcClient;

    public Optional<Profile> getProfileById(int id) {
        // ...
    }
}

Обратите внимание, что ВСЕ репозитории снабжаются аннотацией @Repository, которая является частным случаем @Component. Она обеспечивает маппинг ошибок, специфичных для СУБД, в стандартные исключения JDBC.

Для взаимодействия с БД будем использовать JdbcClient, который обладает той же функциональностью, что и JdbcTemplate, но при этом предоставляет более удобное и современное API. Поэтому JdbcClient является рекомендуемой заменой JdbcTemplate.

Сам SQL-запрос для выборки профиля пользователя здесь вынесен в качестве константы в начало класса. Для подстановки целевого id используется именованный параметр с двоеточием в начале, а не простая конкатенация строки и числа. Это позволяет нам сделать запрос более устойчивым к хакерским атакам типа sql injection с одной стороны и более производительным с другой, т.к. СУБД сможет закешировать шаблон данного запроса.

При поиске по id мы будем возвращать Optional. То есть объект может быть в базе, а может и не быть. И в зависимости от контекста это может трактоваться и как ошибка, и как нормальное поведение. Решение о том, ошибка это или нет, будет принимать сервисный слой, который мы модифицируем далее.

Реализация record-класса Profile рассматривалась в предыдущей статье. Его единственное назначение - это отображать поля таблицы в поля класса на Java.

public record Profile(
        int id,
        String firstName,
        String lastName,
        int age
) {
}

Реализация метода getProfileById() довольно простая:

public Optional<Profile> getProfileById(int id) {
    return jdbcClient.sql(SQL_GET_PROFILE_BY_ID)
            .param("id", id)
            .query((rs, _) -> // RowMapper
                    new Profile(
                            rs.getInt("id"),
                            rs.getString("first_name"),
                            rs.getString("last_name"),
                            rs.getInt("age")
                    )
            )
            .optional();
}

Сначала в jdbcClient с помощью метода sql() указываем текст sql-запроса. Затем с помощью метода param() указываем значение параметра id. Параметров в запросе может быть больше одного и каждый из них можно указывать с помощью отдельного вызова param() или же передать сразу всю мапу с параметрами в метод params(). После этого для чтения данных вызываем метод query(), в который передаём лямбду c RowMapper. Эта лямбда принимает на вход resultSet и rowNum. RowMapper позволяет построчно обрабатывать результат запроса. Номер строки нам здесь не нужен, поэтому вместо него пишем нижнее подчёркивание, что означает безымянный параметр.

Лямбда возвращает объект Profile, заполняя его поля с помощью методов объекта ResultSet: getInt() и getString(). В качестве параметра они оба принимают имя колонки из sql-запроса.

Поскольку в этом запросе мы ищем запись по id, мы ожидаем не более одной строки. Также записи может не быть вообще. Поэтому для получения результата используем метод optional(), возвращающий соответствующий объект.

Новый сервисный слой

Теперь осталось только внедрить наш ProfileRepository в сервисный слой. В предыдущей статье мы уже создавали реализацию сервисного слоя ProfileServiceMock, которая является заглушкой и на самом деле ни в какую базу не ходит. Сейчас мы создадим другую («честную») реализацию того же сервиса:

@Slf4j
@Primary
@Service
@RequiredArgsConstructor
public class ProfileServiceImpl implements ProfileService {

    private final ProfileRepository profileRepository;

    @Override
    public Profile getProfile(int personId) {
        log.info("Get profile by personId={}", personId);
        return profileRepository.getProfileById(personId)
                .orElseThrow(() -> new ProfileNotFoundException(personId));
    }
}

Обратите внимание на аннотацию @Primary. Если её не указывать, то спринг не сможет заинжектить в ProfileController нужную нам реализацию сервиса, т.к. по факту у нас их теперь две. Чтобы указать, что по умолчанию нам нужна именно эта реализация, мы и используем данную аннотацию. Ещё разные реализации интерфейса можно подставлять в зависимости от профиля приложения с помощью аннотации @Profile.

Как я уже говорил, именно сервисный слой «знает» контекст выполнения запроса и может правильно трактовать пустой результат из репозитория. В данном случае пустой результат - это ошибка и здесь Optional предоставляет удобный метод orElseThrow(), в который мы передаём наше исключение через лямбда-выражение.

На этом примере с двумя реализациями одного интерфейса хорошо виден принцип модульности, которого стоит придерживаться при разработке любых приложений на Spring.

ProfileService, в свою очередь, вызывается из контроллера. Таким образом, вырисовывается типичная трёхслойная архитектура: контроллер (с аннотацией @Controller) -> сервис (@Service) -> репозиторий (@Repository). Контроллер отвечает за маппинг входящих http-запросов, сервисный слой реализует бизнес-логику, а репозиторий работает непосредственно с БД.

Теперь, если вы запустите приложение и выполните GET-запрос по адресу http://localhost:8080/profiles/1, то получите профиль с id = 1:

{
  "id": 1,
  "firstName": "Иван",
  "lastName": "Петров",
  "age": 21
}

Если же выполнить запрос с другим id, то наш ErrorController корректно обработает исключение ProfileNotFoundException и выдаст пользователю json с описанием ошибки:

{
  "message": "Profile with id = 111 not found"
}

Итоги

В результате мы добавили в наше приложение слой взаимодействия с БД, а также создали новую реализацию сервисного слоя, который вместо заглушки теперь использует репозиторий.

В следующей статье Добавление записи через POST-запрос в Spring Boot мы научимся создавать записи в БД.


См. также

Облако тэгов

Kotlin, Java, Spring, Spring Boot, Spring Data, Spring AI, SQL, PostgreSQL, Oracle, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.

Последние статьи


Комментарии

29.06.2022 13:25 Артур

Запускаю через Idea, с указанным параметром в моем случае --spring.config.location=/Users/arthurchebotkov/Desktop/SpringBootRestfulService/spring-boot-restful-service/src/main/resources/application.config
Приложение не запускается, пишет в лог:
[main] ru.devmark.app.RestfulApplication        : No active profile set, falling back to default profiles: default

Подскажите, с чем может быть связано?

29.06.2022 13:29 devmark

А у вас точно не запускается? Потому что "No active profile set" - это не ошибка, а всего лишь предупреждение, что профиль не указан явно и используется профиль default.

Если профиль явно не выставлен, то приложение всё равно запустится. Но профиль также можно выставить через параметры запуска.

29.06.2022 13:46 Артур

Не запускается: ERROR 9976 --- [         main] o.s.boot.SpringApplication             : Application startup failed
Насколько я понял указывает на следующие ошибки:
1) Cannot load configuration class: ru.devmark.app.RestfulApplication
2) java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @7791a895
3) Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @7791a895

29.06.2022 15:10 devmark

А на какой версии Java вы этот пример запускаете? Изначально он создавался под Java 8. Судя по тексту ошибки, ругается на то, что в проекте недоступен модуль java.lang.

Но вообще данный пример немного устарел. У меня на YouTube канале есть актуальная серия видео как создавать restful приложение на jdbc: https://youtu.be/hta62ffKcK4. Также есть аналогичное про Spring Data https://youtu.be/BSJcA6IHIZw.

29.06.2022 16:47 Артур

Запускаю на Java 18.
Просто хотел запустить проект на Java+Maven.

Спасибо за информацию!

01.07.2022 00:26 devmark

Я переработал статью и исходники проекта под актуальную версию Spring Boot и Java 17.

02.07.2022 12:13 Артур

Огромное спасибо! :)

Добавить комментарий

×

devmark.ru