Статьи
Утилиты 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 обладают достаточной гибкостью и вы всегда можете написать свою собственную логику заполнения полей аудита.



Комментарии

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

×

devmark.ru