Статьи
YouTube-канал

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

Исходники

2 июля 2022

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

Содержание

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

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

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

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

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

CREATE TABLE public.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 - это драйвер для работы с соответствующей СУБД. Обратите внимание, что он имеет scope = runtime. То есть зависимость нужна только в процессе работы приложения.

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

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

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

Если в проекте по умолчанию был файл application.properties - удалите его.

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

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

Для работы с БД принято выделять отдельный слой repository. Как и для сервиса из предыдущей статьи, здесь будет удобно выделить интерфейс:

public interface ProfileRepository {

    Optional<Profile> getProfileById(int id);
}

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

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

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

Реализация репозитория:

@Repository
public class ProfileRepositoryImpl implements ProfileRepository {

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

    private final ProfileMapper profileMapper;
    private final NamedParameterJdbcTemplate jdbcTemplate;

    public ProfileRepositoryImpl(
            ProfileMapper profileMapper,
            NamedParameterJdbcTemplate jdbcTemplate
    ) {
        this.profileMapper = profileMapper;
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public Optional<Profile> getProfileById(int id) {
        var params = new MapSqlParameterSource();
        params.addValue("id", id);
        return jdbcTemplate.query(
                        SQL_GET_PROFILE_BY_ID,
                        params,
                        profileMapper
                ).stream()
                .findFirst();
    }
}

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

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

NamedParameterJdbcTemplate - стандартный компонент, предоставляющий методы для взаимодействия с БД. Как видно из названия, он поддерживает именованные параметры. ProfileMapper преобразует данные, полученные из БД в объект Profile. То есть он хранит в себе логику маппинга полей таблицы на поля класса. Более подробно мы рассмотрим его чуть ниже.

Реализация нашего целевого метода getProfileById() предельно проста. Сначала подставляем требуемый id в sql-запрос через именованный параметр благодаря классу MapSqlParameterSource. Затем вызываем метод query, передавая ему сам sql-запрос, именованные параметры и маппер полей таблицы. В качестве результата получаем список объектов типа Profile.

Исходя из того, что id является первичным ключом в таблице и его значение уникально, мы можем здесь использовать преобразование в стрим и findFirst(). Такая связка более универсальна и безопасна, чем queryForObject(), который может выкинуть исключение, если найдено более одной записи.

Маппинг результатов выборки из БД

Сам ProfileMapper не хранит внутреннего состояния и всего лишь реализует интерфейс RowMapper, типизированный нашим объектом Profile.

@Component
public class ProfileMapper implements RowMapper<Profile> {

    @Override
    public Profile mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Profile(
                rs.getInt("id"),
                rs.getString("first_name"),
                rs.getString("last_name"),
                rs.getInt("age")
        );
    }
}

На вход он получает ResultSet, представляющий собой результат выборки. Из этого ResultSet мы извлекаем значения полей благодаря методам getInt() и getString() по имени колонки в таблице.

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

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

@Primary
@Service
public class ProfileServiceImpl implements ProfileService {

    private final ProfileRepository profileRepository;

    public ProfileServiceImpl(ProfileRepository profileRepository) {
        this.profileRepository = profileRepository;
    }

    @Override
    public Profile getProfile(int personId) {
        return profileRepository.getProfileById(personId)
                .orElseThrow(() -> new ProfileNotFoundException(personId));
    }
}

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

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

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

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

Теперь если вы запустите приложение и выполните 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, Java 16, Java 11, Java 10, Java 9, Java 8, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, 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