16 ноября 2024
Тэги: gradle, Kotlin, rest, Spring Data, SQL.
В промышленных системах бывает важно знать, кто и когда сделал изменения конкретной сущности. Прежде всего нас интересует такая информация:
Рассмотрим пример, в котором у нас есть таблица company. У компании есть id (автоинкремент) и название.
Поля аудита обычно скрыты от рядовых пользователей, но доступны администраторам системы. Скрипт создания этой таблицы может выглядеть так:
Обратите внимание, что наличие данных в полях аудита здесь не обязательно. Сделано это для того, чтобы в случае каких-то проблем с аудитом не потерялись «бизнесовые» изменения.
Создадим типовой gradle-проект на Kotlin и Spring Boot и добавим туда зависимости Spring Web и Spring Data JPA. В итоге у вас в проекте должны быть прописаны следующие зависимости:
Также не забудьте добавить сюда драйвер вашей СУБД и прописать настройки подключения к БД в application.yml. Пример проекта доступен на github.
Создадим типовую JPA-сущность для хранения информации о компании. Из-за особенностей работы Spring Data для сущностей мы вынуждены использовать обычный класс, а не более логичный для таких случаев data class. Причём все поля должны иметь модификатор var.
К ней создадим интерфейс JPA-репозитория, который даст основной функционал для чтения и записи данных:
Также создадим DTO (data transfer object), в котором будет передаваться имя компании в запросе. DTO должен содержать только те поля, которые может редактировать пользователь. Это очень полезный приём отделения сущности БД от REST API.
Теперь создадим rest-контроллер, в который внедрим CompanyRepository. Контроллер будет доступен по эндпоинту /companies.
Добавим в контроллер обработчик POST-запроса. Он будет создавать новую компанию.
Здесь мы всегда создаём новую сущность CompanyEntity, инициализируя её имя тем значением, которые пришло к нам в теле запроса в dto. Затем вызываем метод save() у репозитория – он выполнит вставку новой записи в таблицу company.
Также добавим обработчик PUT-запроса.
В урле этого запроса будет содержаться id существующей записи, а в теле запроса – новое значение для имени компании. Перед обновлением сначала пытаемся проверить, существует ли такая запись? Если нет – кидаем ошибку. Затем устанавливаем новое значение имени и сохраняем изменения.
Также для наглядности сделаем обработчик GET-запроса. Он будет возвращать список всех записей, которые уже есть в таблице.
В принципе это вся «бизнесовая» реализация нашей логики. Если сейчас запустить приложение и выполнить запросы, то мы уже можем создавать новые компании и редактировать названия у существующих. Но при этом поля аудита пока будут пустыми. Как же их заполнять?
Чтобы не прописывать в каждой JPA-сущности однотипные поля аудита, можно вынести их в базовый класс, чтобы затем наследовать от него только те сущности, для которых требуется аудит. Назовём этот класс Auditable:
Аннотация @MappedSuperclass указывает на то, что поля этой сущности нужно добавить ко всем сущностям-наследникам как если бы они были объявлены в них в явном виде. Отсюда следует, что для каждой сущности с аудитом нужно не забыть добавить эти 4 поля в таблицу БД.
@EntityListeners позволяет указать класс, который будет выполнять инициализацию полей аудита перед сохранением сущности. Здесь мы используем стандартный спринговый класс AuditingEntityListener, но при необходимости вы можете определить и свой.
В самой сущности перечисляем 4 поля: кто создал запись, когда создал, кто отредактировал и когда отредактировал. Все они помечаются соответствующими стандартными аннотациями. Обратите внимание, что все поля имеют значения по умолчанию и модификатор var. Сам класс Auditable при этом является абстрактным.
Теперь нам надо добавить класс спринговой конфигурации AuditConfig.
@EnableJpaAuditing включает механизм аудита для JPA-сущностей. Внутри конфигурации мы создаём бин auditorProvider. Этот бин возвращает тип AuditorAware. По сути это должна быть строка, содержащая логин пользователя, выполняющего текущие изменения. В данном примере здесь хардкод, но в реальном приложении вы можете прописать здесь любую логику получения текущего пользователя. Скорее всего, вы будете использовать объект SecurityContext из Spring Security.
Поля аудита в таблице уже есть, базовый класс создали, аудит настроили и теперь осталось лишь унаследовать нашу бизнесовую сущность CompanyEntity от класса Auditable:
И это всё! Запускаем приложением и тестируем работу аудита. При создании новой сущности через POST-запрос будут заполняться все 4 поля аудита (created_by, created_date, updated_by, updated_date), а при редактировании через PUT – только 2 (updated_by и updated_date).
Кстати, при выполнении PUT-запроса, поля аудита будут меняться только в том случае, если вы действительно изменили имя компании. Spring Data настолько умный, что отслеживает реальные различия между исходными данными и новыми. И если они совпадают, то даже не будет выполнять update. А раз нет обновления «бизнесовых» полей, то не будут меняться и поля аудита.
Мы на конкретном примере убедились, что Spring Data предоставляет простой механизм аудита изменений JPA-сущностей. Поскольку поля аудита во всех сущностях одинаковые, мы вынесли их в базовый абстрактный класс и максимально упростили подключение аудита. Всё, что нужно будет сделать – это унаследовать целевую сущность от этого базового класса.
При этом стандартные механизмы Spring обладают достаточной гибкостью и вы всегда можете написать свою собственную логику заполнения полей аудита.
Kotlin, Java, 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.
18.11.2024 23:50 devmark
Актуализировал статью про аудит изменений. Перевёл репозиторий на github на Spring Boot 3 и Java 21.