Аудит пользователей Spring Data JPA

—

от автора

Задача: в БД необходимо фиксировать кто создал сущность, кто её обновил, и кто её удалил.

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

Мне хотелось полностью делегировать это приложению, но погуглив, я не нашёл какого‑то явного решения. Сейчас расскажу, как мне удалось это сделать:

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


ссылка на оригинал статьи https://habr.com/ru/articles/757866/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *