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

Мне хотелось полностью делегировать это приложению, но погуглив, я не нашёл какого‑то явного решения. Сейчас расскажу, как мне удалось это сделать:
Допустим у нас есть таблица с животными, которая содержит 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, и аудит пользователей будет работать. Больше не нужно добавлять аннотации ко всем методам, которые вносят изменения в сущность, кроме того, нет необходимости по сервисному слою таскать сервис получения пользователя из контекста и «сетать» его в необходимые поля. Приложение будет автоматически отслеживать пользователей, которые изменили сущность.