Как стать автором
Обновить
173.82
НЛМК ИТ
Группа НЛМК

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

Время на прочтение5 мин
Количество просмотров6K

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

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

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

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

Теги:
Хабы:
Всего голосов 11: ↑10 и ↓1+10
Комментарии19

Публикации

Информация

Сайт
nlmk.com
Дата регистрации
Дата основания
2013
Численность
свыше 10 000 человек
Местоположение
Россия