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, H2, Linux, Hibernate, Collections, Stream API, многопоточность, чат-боты, нейросети, файлы, devops, Docker, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.
18.11.2024 23:50 devmark
Актуализировал статью про аудит изменений. Перевёл репозиторий на github на Spring Boot 3 и Java 21.