Перейти к основному содержимому

Аудит с помощью JPA, Hibernate и Spring Data JPA

· 9 мин. чтения

1. Обзор

В контексте ORM аудит базы данных означает отслеживание и регистрацию событий, связанных с постоянными сущностями, или просто управление версиями сущностей. Вдохновленные триггерами SQL, события представляют собой операции вставки, обновления и удаления сущностей. Преимущества аудита базы данных аналогичны преимуществам контроля версий исходного кода.

В этом руководстве мы продемонстрируем три подхода к внедрению аудита в приложение. Во-первых, мы реализуем его с помощью стандартного JPA. Далее мы рассмотрим два расширения JPA, которые предоставляют свои собственные функции аудита, одно предоставляется Hibernate, другое — Spring Data.

Вот примеры связанных сущностей, Bar и Foo, которые мы будем использовать в этом примере:

./3e074f86df4e5666a86df76786131bb8.png

2. Аудит с помощью JPA

JPA не содержит явно API аудита, но мы можем реализовать эту функциональность, используя события жизненного цикла объекта.

2.1. @PrePersist, @PreUpdate и @PreRemove

В классе JPA Entity мы можем указать метод в качестве обратного вызова, который мы можем вызывать во время определенного события жизненного цикла объекта. Поскольку нас интересуют обратные вызовы, выполняемые до соответствующих операций DML, аннотации обратного вызова @PrePersist , @PreUpdate и @PreRemove доступны для наших целей:

@Entity
public class Bar {

@PrePersist
public void onPrePersist() { ... }

@PreUpdate
public void onPreUpdate() { ... }

@PreRemove
public void onPreRemove() { ... }

}

Внутренние методы обратного вызова всегда должны возвращать значение void и не принимать аргументов. Они могут иметь любое имя и любой уровень доступа, но не должны быть статическими .

Имейте в виду, что аннотация @Version в JPA не имеет строгого отношения к нашей теме; это больше связано с оптимистичной блокировкой, чем с данными аудита.

2.2. Реализация методов обратного вызова

Однако у этого подхода есть существенное ограничение. Как указано в спецификации JPA 2 (JSR 317):

Как правило, метод жизненного цикла переносимого приложения не должен вызывать операции EntityManager или Query , обращаться к другим экземплярам сущностей или изменять отношения в одном и том же контексте сохраняемости. Метод обратного вызова жизненного цикла может изменить состояние отсутствия связи объекта, для которого он вызывается.

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

@Entity
public class Bar {

//...

@Column(name = "operation")
private String operation;

@Column(name = "timestamp")
private long timestamp;

//...

// standard setters and getters for the new properties

//...

@PrePersist
public void onPrePersist() {
audit("INSERT");
}

@PreUpdate
public void onPreUpdate() {
audit("UPDATE");
}

@PreRemove
public void onPreRemove() {
audit("DELETE");
}

private void audit(String operation) {
setOperation(operation);
setTimestamp((new Date()).getTime());
}

}

Если нам нужно добавить такой аудит к нескольким классам, мы можем использовать @EntityListeners для централизации кода:

@EntityListeners(AuditListener.class)
@Entity
public class Bar { ... }
public class AuditListener {

@PrePersist
@PreUpdate
@PreRemove
private void beforeAnyOperation(Object object) { ... }

}

3. Спящий режим

С помощью Hibernate мы можем использовать Interceptors и EventListeners, а также триггеры базы данных для выполнения аудита. Но платформа ORM предлагает Envers, модуль, реализующий аудит и управление версиями постоянных классов.

3.1. Начните с Envers

Чтобы настроить Envers, нам нужно добавить JAR hibernate-envers в наш путь к классам:

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate.version}</version>
</dependency>

Затем мы добавляем аннотацию @Audited либо к @Entity (для аудита всего объекта), либо к конкретным @Column s (если нам нужно проверять только определенные свойства):

@Entity
@Audited
public class Bar { ... }

Обратите внимание, что Bar имеет отношения «один ко многим» с Foo . В этом случае нам нужно либо проверить Foo , добавив @Audited в Foo, либо установить @NotAudited в свойстве отношения в Bar :

@OneToMany(mappedBy = "bar")
@NotAudited
private Set<Foo> fooSet;

3.2. Создание таблиц журнала аудита

Существует несколько способов создания таблиц аудита:

  • установите в hibernate.hbm2ddl.auto значения create , create-drop или update , чтобы Envers мог создавать их автоматически
  • используйте org.hibernate.tool.EnversSchemaGenerator для программного экспорта полной схемы базы данных
  • настроить задачу Ant для создания соответствующих операторов DDL
  • использовать плагин Maven для создания схемы базы данных из наших сопоставлений (например, Juplo) для экспорта схемы Envers (работает с Hibernate 4 и выше)

Мы пойдем по первому пути, так как он самый простой, но имейте в виду, что использование hibernate.hbm2ddl.auto небезопасно в рабочей среде.

В нашем случае таблицы bar_AUD и foo_AUD (если мы также установили Foo как @Audited ) должны генерироваться автоматически. Таблицы аудита копируют все проверенные поля из таблицы объекта с двумя полями, REVTYPE (значения: «0» для добавления, «1» для обновления и «2» для удаления объекта) и REV .

Кроме того, по умолчанию будет сгенерирована дополнительная таблица с именем REVINFO . Он включает два важных поля, REV и REVTSTMP, и записывает отметку времени каждой версии. Как мы можем догадаться, bar_AUD.REV и foo_AUD.REV на самом деле являются внешними ключами REVINFO.REV.

3.3. Настройка конвертов

Мы можем настроить свойства Envers так же, как и любое другое свойство Hibernate.

Например, давайте изменим суффикс таблицы аудита (по умолчанию « _AUD ») на « _AUDIT_LOG». Вот как мы устанавливаем значение соответствующего свойства org.hibernate.envers.audit_table_suffix :

Properties hibernateProperties = new Properties(); 
hibernateProperties.setProperty(
"org.hibernate.envers.audit_table_suffix", "_AUDIT_LOG");
sessionFactory.setHibernateProperties(hibernateProperties);

Полный список доступных свойств можно найти в документации Envers .

3.4. Доступ к истории объекта

Мы можем запрашивать исторические данные аналогично запросу данных через Hibernate Criteria API. Мы можем получить доступ к истории аудита объекта с помощью интерфейса AuditReader , который мы можем получить с помощью открытого EntityManager или Session через AuditReaderFactory :

AuditReader reader = AuditReaderFactory.get(session);

Энверс предоставляет AuditQueryCreator (возвращенный AuditReader.createQuery() ) для создания запросов, специфичных для аудита. Следующая строка вернет все экземпляры Bar , измененные в ревизии № 2 (где bar_AUDIT_LOG.REV = 2 ):

AuditQuery query = reader.createQuery()
.forEntitiesAtRevision(Bar.class, 2)

Вот как мы можем запросить ревизии Bar . Это приведет к получению списка всех проверенных экземпляров Bar во всех их состояниях:

AuditQuery query = reader.createQuery()
.forRevisionsOfEntity(Bar.class, true, true);

Если второй параметр равен false, результат объединяется с таблицей REVINFO . В противном случае возвращаются только экземпляры сущности. Последний параметр указывает, следует ли возвращать удаленные экземпляры Bar .

Затем мы можем указать ограничения, используя фабричный класс AuditEntity :

query.addOrder(AuditEntity.revisionNumber().desc());

4. Спринг-данные JPA

Spring Data JPA — это фреймворк, который расширяет JPA, добавляя дополнительный уровень абстракции поверх поставщика JPA. Этот уровень поддерживает создание репозиториев JPA путем расширения интерфейсов репозиториев Spring JPA.

Для наших целей мы можем расширить CrudRepository<T, ID extends Serializable> , интерфейс для общих операций CRUD. Как только мы создали и внедрили наш репозиторий в другой компонент, Spring Data автоматически предоставит реализацию, и мы готовы добавить функции аудита.

4.1. Включение аудита JPA

Для начала мы хотим включить аудит с помощью конфигурации аннотаций. Для этого мы добавляем @EnableJpaAuditing в наш класс @Configuration :

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories
@EnableJpaAuditing
public class PersistenceConfig { ... }

4.2. Добавление прослушивателя обратного вызова Spring Entity

Как мы уже знаем, JPA предоставляет аннотацию @EntityListeners для указания классов слушателей обратного вызова. Spring Data предоставляет свой собственный класс прослушивателя сущностей JPA, AuditingEntityListener . Итак, давайте укажем прослушиватель для объекта Bar :

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar { ... }

Теперь мы можем собирать информацию аудита прослушивателем при сохранении и обновлении объекта Bar .

4.3. Отслеживание дат создания и последнего изменения

Далее мы добавим два новых свойства для хранения даты создания и даты последнего изменения в нашей сущности Bar . Свойства аннотируются аннотациями @CreatedDate и @LastModifiedDate соответственно, а их значения устанавливаются автоматически:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {

//...

@Column(name = "created_date", nullable = false, updatable = false)
@CreatedDate
private long createdDate;

@Column(name = "modified_date")
@LastModifiedDate
private long modifiedDate;

//...

}

Как правило, мы перемещаем свойства в базовый класс (с аннотацией @MappedSuperClass ), который будет расширяться всеми нашими проверенными объектами. В нашем примере мы добавляем их непосредственно в Bar для простоты.

4.4. Аудит автора изменений с помощью Spring Security

Если наше приложение использует Spring Security, мы можем отслеживать, когда вносятся изменения и кто их сделал:

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Bar {

//...

@Column(name = "created_by")
@CreatedBy
private String createdBy;

@Column(name = "modified_by")
@LastModifiedBy
private String modifiedBy;

//...

}

Столбцы, аннотированные @CreatedBy и @LastModifiedBy , заполняются именем принципала, который создал или последним изменил сущность. Информация поступает из экземпляра аутентификации SecurityContext . Если мы хотим настроить значения, заданные для аннотированных полей, мы можем реализовать интерфейс AuditorAware<T> :

public class AuditorAwareImpl implements AuditorAware<String> {

@Override
public String getCurrentAuditor() {
// your custom logic
}

}

Чтобы настроить приложение для использования AuditorAwareImpl для поиска текущего принципала, мы объявляем bean-компонент типа AuditorAware , инициализированный экземпляром AuditorAwareImpl, и указываем имя bean-компонента в качестве значения параметра auditAwareRef в @EnableJpaAuditing :

@EnableJpaAuditing(auditorAwareRef="auditorProvider")
public class PersistenceConfig {

//...

@Bean
AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}

//...

}

5. Вывод

В этой статье мы рассмотрели три подхода к реализации функционала аудита:

  • Чистый подход JPA является самым простым и состоит из использования обратных вызовов жизненного цикла. Однако нам разрешено изменять только не связанное с отношениями состояние объекта. Это делает обратный вызов @PreRemove бесполезным для наших целей, так как любые настройки, которые мы сделали в методе, будут удалены вместе с сущностью.
  • Envers — это зрелый модуль аудита, предоставляемый Hibernate. Он легко настраивается и лишен недостатков чистой реализации JPA. Таким образом, это позволяет нам проверять операцию удаления, поскольку она регистрируется в таблицах, отличных от таблицы сущности.
  • Подход Spring Data JPA абстрагирует работу с обратными вызовами JPA и предоставляет удобные аннотации для аудита свойств. Он также готов к интеграции с Spring Security. Недостатком является то, что он наследует те же недостатки подхода JPA, поэтому операцию удаления нельзя проверить.

Примеры для этой статьи доступны в репозитории GitHub .