Задача: в БД необходимо фиксировать кто создал сущность, кто её обновил, и кто её удалил.
Все знают, как взять пользователя из контекста и сунуть его в сущность. Допустим, на уровне сервиса в методе извлечь информацию о нём и «засетать» его в нужные поля (придётся везде таскать этот кусок кода по сервису), а с аспектами как‑то выглядит не явно и накладывает ряд обязательств (например, развешивание аннотаций над методами всякий раз, когда мы что‑то пытаемся сделать с сущностью (новые участники команды могут не знать о такой неявной практике, а старые забыть о ней)).

Мне хотелось полностью делегировать это приложению, но погуглив, я не нашёл какого‑то явного решения. Сейчас расскажу, как мне удалось это сделать:
Допустим у нас есть таблица с животными, которая содержит name, и уникальный код, который берётся из паспорта животного.
Давайте для начала создадим таблицу:
<changeSet id="create-table-animal.xml" author="alexander"> <createTable tableName="animal"> <column name="id" type="UUID"> <constraints nullable="false" primaryKey="true"/> </column> <column name="name" type="VARCHAR(255)"> <constraints nullable="false"/> </column> <column name="code" type="VARCHAR(255)"> <constraints nullable="false"/> </column> <column name="created_at" type="TIMESTAMP"> <constraints nullable="false"/> </column> <column name="updated_at" type="TIMESTAMP"> </column> <!— У удалённых сущностей будем просто проставлять дату удаления--> <column name="deleted_at" type="TIMESTAMP"> </column> <!— кто создал запись--> <column name="created_by" type="VARCHAR(255)"> </column> <column name="created_by_mail" type="VARCHAR(255)"> </column> <!— кто обновил запись--> <column name="updated_by" type="VARCHAR(255)"> </column> <column name="updated_by_mail" type="VARCHAR(255)"> </column> <!— кто удалил запись--> <column name="deleted_by" type="VARCHAR(255)"> </column> <column name="deleted_by_mail" type="VARCHAR(255)"> </column> </createTable> <!-- уникальный код только для не удалённых животных--> <sql> create unique index unique_code on animal(code) where deleted_at is null; </sql> </changeSet>

Создадим стандартный суперкласс, который будет фиксировать дату создания и дату изменения, от которого будут наследоваться все сущности:
/** * Общий каркас для всех сущностей. */ @Getter @ToString(onlyExplicitlyIncluded = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @NoArgsConstructor @MappedSuperclass @EntityListeners(AuditingEntityListener.class) public abstract class AbstractAuditableEntity { @CreatedDate @Column(updatable = false) @Type(type = "java.time.Instant") @EqualsAndHashCode.Include @ToString.Include private Instant createdAt; @LastModifiedDate @Type(type = "java.time.Instant") @EqualsAndHashCode.Include @ToString.Include private Instant updatedAt; }
Теперь создадим суперкласс для аудита пользователей:
/** * Суперкласс для аудита пользователей. */ @Setter @Getter @ToString(onlyExplicitlyIncluded = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @NoArgsConstructor @MappedSuperclass @EntityListeners(AuditUserListener.class) public class AuditUser extends AbstractAuditableEntity { /** * ФИО кто создал. */ @ToString.Include @Column(updatable = false) private String createdBy; /** * email кто создал. */ @ToString.Include @Column(updatable = false) private String createdByMail; /** * ФИО кто обновил. */ @ToString.Include private String updatedBy; /** * email кто обновил. */ @ToString.Include private String updatedByMail; /** * ФИО кто удалил. */ @ToString.Include private String deletedBy; /** * email кто удалил. */ @ToString.Include private String deletedByMail; }
Обратите внимание на AuditUserListener.class – его уже напишем мы сами.
@Configurable @RequiredArgsConstructor(onConstructor_ = {@Lazy}) public class AuditUserListener { /** * Сервис, который отвечает за получение пользователя из контекста */ private final UserBuilderFromContext userBuilderFromContext; private final EntityManager entityManager; @PrePersist private void beforeAnyCreate(AuditUser audit) { var user = userBuilderFromContext.getUserFromContext(); audit.setCreatedBy(user.getFullName()); audit.setCreatedByMail(user.getEmail()); } @PreUpdate private void beforeAnyUpdate(AuditUser audit) { var user = userBuilderFromContext.getUserFromContext(); audit.setUpdatedBy(user.getFullName()); audit.setUpdatedByMail(user.getEmail()); } @PreRemove private void beforeAnyRemove(AuditUser audit) { var user = userBuilderFromContext.getUserFromContext(); audit.setDeletedBy(user.getFullName()); audit.setDeletedByMail(user.getEmail()); // при удалении сущность дёргается из бд по id но save не вызывается // при удалении вызывается sql запрос проставляющий дату удаления // по этому нужен entityManager чтобы синхронизировать кеш с бд entityManager.flush(); } }
Внимание!: процедура проставления пользователя работает только в транзакции. Но т.к. мы работаем в многопоточном приложении, правильно, что все CRUD операции будут выполняться транзакционно.
Мы видим, что использовалась ленивая инициализация бинов:
@RequiredArgsConstructor(onConstructor_ = {@Lazy}
Если этого не сделать, получим проблему циклической зависимости:

Давайте отнаследуемся от суперкласса AuditUser, создав нашу entity Animal.
Если мы хотим фиксировать пользователя, который удалил сущность, то фактически удалять из бд её не будем, а значит надо будет настроить мягкое удаление (Soft delete).
/** * Entity Animal */ @Getter @Setter @ToString(onlyExplicitlyIncluded = true) @NoArgsConstructor @EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false) @Entity @Table(name = "animal") // Мягкое удаление @SQLDelete(sql = "update animal set deleted_at=now() AT TIME ZONE 'UTC' where id = ?") // Фильтр, чтобы не получать удалённые сущности из бд @Where(clause = " deleted_at is null ") public class Animal extends AuditUser { /** * Идентификатор. */ @Id @GeneratedValue @ToString.Include @EqualsAndHashCode.Include protected UUID id; /** * Код животного по паспорту */ @Column @ToString.Include private String code; /** * Имя животного */ @Column @ToString.Include private String name; @Type(type = "java.time.Instant") @EqualsAndHashCode.Include @ToString.Include private Instant deletedAt; }
Проверим:
curl -X 'POST' \ 'http://localhost:8080/api/v1/animal' \ -H 'accept: */*' \ -H 'Authorization: Bearer ТУТ МОЙ JWT TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Мурзик", "code": "50 05 984 929" }'
curl -X 'PUT' \ 'http://localhost:8080/api/v1/animal' \ -H 'accept: */*' \ -H 'Authorization: Bearer ТУТ МОЙ JWT TOKEN' \ -d '{ "name": "Барсик", "code": "50 05 984 929", "id": "d085d72a-b15f-491b-8dbc-f6dbe3dba2e1" }'
curl -X 'DELETE' \ 'http://localhost:8080/api/v1/animal' \ -H 'accept: */*' \ -H 'Authorization: Bearer ТУТ МОЙ JWT TOKEN' \ -H 'Content-Type: application/json' \ -d '["d085d72a-b15f-491b-8dbc-f6dbe3dba2e1"]'

Результат
Теперь мы можем создавать любую Entity (операции, сертификаты, тикеты), наследуясь от AuditUser, и аудит пользователей будет работать. Больше не нужно добавлять аннотации ко всем методам, которые вносят изменения в сущность, кроме того, нет необходимости по сервисному слою таскать сервис получения пользователя из контекста и «сетать» его в необходимые поля. Приложение будет автоматически отслеживать пользователей, которые изменили сущность.
