Статьи Утилиты Telegram YouTube Отзывы

Аудит изменений в Spring Data JPA

Видеогайд Исходники

4 мая 2023

Тэги: gradle, Kotlin, PostgreSQL, rest, Spring Data, SQL.

Содержание

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

В промышленных системах бывает важно знать, кто и когда сделал изменения конкретной сущности. Прежде всего нас интересует такая информация:

  • кто создал запись
  • дата создания
  • автор последних изменений
  • дата последних изменений

Структура таблицы

Рассмотрим пример, в котором у нас есть таблица company. У компании есть id (автоинкремент) и название.

Поля аудита обычно скрыты от рядовых пользователей, но доступны администраторам системы. Скрипт создания этой таблицы в СУБД Postgres будет выглядеть так:

create table company
(
    id           serial constraint company_pk primary key,
    name         varchar not null,
    created_date timestamp,
    updated_date timestamp,
    created_by   varchar,
    updated_by   varchar
);

Обратите внимание, что наличие данных в полях аудита здесь не обязательно. Сделано это для того, чтобы в случае каких-то проблем с аудитом не потерялись «бизнесовые» изменения.

Заготовка проекта

Создадим типовой gradle-проект на Kotlin и Spring Boot и добавим туда зависимости Spring Web и Spring Data JPA. В итоге у вас в проекте должны быть прописаны следующие зависимости:

implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
runtimeOnly("org.postgresql:postgresql") // драйвер Postgres

Также не забудьте добавить сюда драйвер вашей СУБД и прописать настройки подключения к БД в application.yml. Пример проекта доступен на github.

Сущность базы данных

Создадим типовую JPA-сущность для хранения информации о компании. Из-за особенностей работы Spring Data для сущностей мы вынуждены использовать обычный класс, а не более логичный для таких случаев data class. Причём все поля должны иметь модификатор var.

@Entity
@Table(name = "company")
class CompanyEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Int = 0,

    var name: String,
)

К ней создадим интерфейс JPA-репозитория, который даст основной функционал для чтения и записи данных:

interface CompanyRepository : CrudRepository<CompanyEntity, Int>

Также давайте создадим DTO (data transfer object), в котором будет передаваться имя компании в запросе. DTO должен содержать только те поля, которые может редактировать пользователь. Это очень полезный приём отделения сущности БД от REST API.

data class CompanyDto(
    val name: String,
)

API для создания и редактирования записей

Теперь создадим rest-контроллер, в который внедрим CompanyRepository. Контроллер будет доступен по урлу /companies.

@RestController
@RequestMapping("/companies")
class CompanyController(
    private val companyRepository: CompanyRepository,
) {
}

Добавим в контроллер обработчик POST-запроса. Он будет создавать новую компанию.

@PostMapping
fun createCompany(@RequestBody dto: CompanyDto) {
    val entity = CompanyEntity(
        name = dto.name,
    )
    companyRepository.save(entity)
}

Здесь мы всегда создаём новую сущность CompanyEntity, инициализируя её имя тем значением, которые пришло к нам в теле запроса в dto. Затем вызываем метод save() у репозитория – он выполнит вставку новой записи в таблицу company.

Также добавим обработчик PUT-запроса.

@PutMapping("/{id}")
fun updateCompany(@PathVariable id: Int, @RequestBody dto: CompanyDto) {
    val entity = companyRepository.findByIdOrNull(id)
        ?: throw RuntimeException("Company not found")
    entity.name = dto.name
    companyRepository.save(entity)
}

В урле этого запроса будет содержаться id существующей записи, а в теле запроса – новое значение для имени компании. Перед обновлением сначала пытаемся проверить, существует ли такая запись? Если нет – кидаем ошибку. Затем устанавливаем новое значение имени и сохраняем изменения.

В принципе это вся «бизнесовая» реализация нашей логики. Если сейчас запустить приложение и выполнить запросы, то мы уже можем создавать новые компании и редактировать названия у существующих. Но при этом поля аудита пока будут пустыми. Как же их заполнять?

Базовая сущность аудита

Чтобы не прописывать в каждой JPA-сущности однотипные поля аудита, можно вынести их в базовый класс, чтобы затем наследовать от него только те сущности, для которых требуется аудит. Назовём этот класс Auditable:

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class Auditable(
    @CreatedBy
    var createdBy: String? = null,

    @CreatedDate
    var createdDate: LocalDateTime? = null,

    @LastModifiedBy
    var updatedBy: String? = null,

    @LastModifiedDate
    var updatedDate: LocalDateTime? = null,
)

Аннотация @MappedSuperclass указывает на то, что поля этой сущности нужно добавить ко всем сущностям-наследникам как если бы они были объявлены в них в явном виде. Отсюда следует, что для каждой сущности с аудитом нужно не забыть добавить эти 4 поля в таблицу БД.

@EntityListeners позволяет указать класс, который будет выполнять инициализацию полей аудита перед сохранением сущности. Здесь мы используем стандартный спринговый класс AuditingEntityListener, но при необходимости вы можете определить и свой.

В самой сущности перечисляем 4 поля: кто создал запись, когда создал, кто отредактировал и когда отредактировал. Все они помечаются соответствующими стандартными аннотациями. Обратите внимание, что все поля имеют значения по умолчанию и модификатор var. Сам класс Auditable при этом является абстрактным.

Теперь нам надо добавить класс спринговой конфигурации AuditConfig.

@Configuration
@EnableJpaAuditing
class AuditConfig {
    @Bean
    fun auditorProvider(): AuditorAware<String> {
        // Здесь должна быть ваша логика определения текущего пользователя
        return AuditorAware { Optional.of("system") }
    }
}

@EnableJpaAuditing включает механизм аудита для JPA-сущностей. Внутри конфигурации мы создаём бин auditorProvider. Этот бин возвращает тип AuditorAware. По сути это должна быть строка, содержащая логин пользователя, выполняющего текущие изменения. В данном примере здесь хардкод, но в реальном приложении вы можете прописать здесь любую логику получения текущего пользователя. Скорее всего, вы будете использовать объект SecurityContext из Spring Security.

Подключаем аудит к целевой сущности

Поля аудита в таблице уже есть, базовый класс создали, аудит настроили и теперь осталось лишь унаследовать нашу бизнесовую сущность CompanyEntity от класса Auditable:

@Entity
@Table(name = "company")
class 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.

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


Комментарии

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

×

devmark.ru