Продолжаем изучать библиотеку Hibernate Envers. В этой статье мы рассмотрим получение информации из истории изменений записи.
Получение данных из истории изменений с помощью AuditQuery
Сохранение истории изменений данных это даже в лучшем случае только половина дела. В случае необходимости нужно уметь получать из неё нужную информацию. Для этого Hibernate Envers предоставляет объект AuditQuery.
Для начала рассмотрим простейший пример – получение версии данных, соответствующих номеру редакции.
Здесь и далее мы будем использовать пример из предыдущей статьи.
1 2 3 4 5 6 7 8 |
public Goods editionByRevision(int revisionId, long id) { // Создаём запрос для редакции с номером (поле «rev» равным revisionId) AuditQuery query = AuditReaderFactory.get(em).createQuery().forEntitiesAtRevision(Goods.class, revisionId); // Добавляем отбор по id записи, которая нас интересует query.add(AuditEntity.id().eq(id)); // Возвращаем результат return (Goods) query.getSingleResult(); } |
Здесь и далее em – это объект EntitityManager, который в Spring или Spring Boot можно получить, внедрив соответствующую зависимость.
1 2 |
@PersistenceContext private EntityManager em; |
Теперь решим более сложную задачу. Получим всю историю изменений для записи целиком.
1 2 3 4 5 6 |
public List<Object[]> getHistoryById(long id) { return AuditReaderFactory.get(em).createQuery() .forRevisionsOfEntity(Goods.class, false, true) .add(AuditEntity.id().eq(id)) .getResultList(); } |
Здесь для создания запроса уже используется метод forRevisionsOfEntity, который формирует список редакций для записей таблицы. Этот метод имеет два важных параметра типа Boolean. Если первый из них (второй по счёту параметр метода) равен true, то запрос вернёт только набор самих данных без информации о редакции. Второй (последний параметр метода) определяет возвращать ли сведения об удалённых записях (если true данные возвращаются). Таким образом в примере выше запрос сконфигурирован таким образом чтобы получить наиболее полную информацию о тех изменениях, которые происходили с записью.
В целях видимо универсальности Hibernate Envers возвращает подобные данные из истории в формате List<Object[]>, что создаёт ощутимое неудобство при работе. Однако объекты в каждом массиве имеют чёткую структуру. Эта структура и правильный разбор таких данных будут рассмотрены ближе к концу статьи.
В завершение нашего знакомства с AudutQuery рассмотрим ещё более сложный вариант. А, именно получение данных из истории изменений по состоянию на указанную дату.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public List<Object[]> editionByDate(LocalDate dateTime, long id) { // Вычисление дат начала и завершения интервала с переводом в миллисекунды. long startDateTimestamp = dateTime .atStartOfDay(ZoneId.systemDefault()).toInstant().getLong(ChronoField.INSTANT_SECONDS) * 1000; long endDateTimestamp = dateTime.plusDays(1) .atStartOfDay(ZoneId.systemDefault()).toInstant().getLong(ChronoField.INSTANT_SECONDS) * 1000; // Собственно сам отбор данных. return AuditReaderFactory.get(em).createQuery() .forRevisionsOfEntity(Goods.class, false, true) .add(AuditEntity.id().eq(id)) .add(AuditEntity.revisionProperty("timestamp").ge(startDateTimestamp)) .add(AuditEntity.revisionProperty("timestamp").lt(endDateTimestamp)) .getResultList(); } |
В этом примере мы добавили отбор по интервалу дат. Но тут есть одна тонкость.
Дело в том, что дата и время создания редакции хранится в БД в формате UNIX Timestamp в миллисекундах в виде целого числа (например, в PostgreSQL это поле имеет тип int8). Поэтому при работе с типами данных даты и времени в Java необходимо предварительно переводить их миллисекунды и только лишь после этого подставлять в запрос.
По этой же причине запрос делается в интервале дат (данные за 1 декабря будут между 0 ч. 0 мин. 1 декабря и 0 ч. 0 мин. 2 декабря).
Альтернативный способ получения данных из истории изменений
В случае нежелания работать с AuditQuery можно создать модель для соответствующих таблиц, которые создаёт Hibernate Envers и использовать для получения данных стандартные средства Hibernate. Однако при большом количестве моделей, изменения в которых необходимо отслеживать, такой подход будет слишком громоздким и только создаст неоправданные сложности и трудозатраты.
Также в очень старых версиях у объекта AuditReader был метод getRevisionForDate, который позволял получать сразу редакции на указанную дату, но на данный момент он уже давным-давно удалён разработчиками.
Интерпретация данных истории изменений
Как уже говорилось выше, формат List<Object[]>, который по умолчанию используется для данных истории изменений не удобен. Но, его можно легко интерпретировать в список объектов определённой структуры.
Эту структуру можно представить в виде следующих двух классов. Первый из них описывает весь набор данных для редакции целиком.
1 2 3 4 5 6 7 8 9 10 |
@AllArgsConstructor @Getter public class RevisionData{ // Для собственно данных таблицы можно сохранить тип Object для универсальности. private Object entity; // Объект с дополнительной информацией о редакции private Revision revision; // Тип редакции (RevisionType – штатное перечисление из состава Hibernate Envers) private RevisionType revisionType; } |
Второй описывает дополнительную информацию о редакции.
1 2 3 4 5 6 7 8 9 10 11 12 |
@AllArgsConstructor @Getter public class Revision { private int id; private long timestamp; public static Revision parse(DefaultRevisionEntity imput) { return new Revision( imput.getId(), imput.getTimestamp() ); } } |
Справедливости ради для последней в Hibernate Envers есть стандартный класс DefaultRevisionEntity, который по своей сути аналогичен приведённому, но им лучше не пользоваться, т.к. вследствие особенностей работы Hibernate с его сериализацией в тот же JSON могут возникнуть сложности связанные с hibernateLazyInitializer.
Перевести List<Object []>, который возвращает Hibernate Envers, в List<RevisionData> можно следующим способом:
1 2 3 4 5 6 7 8 9 10 11 |
private List<RevisionData> toRevisionDataList(List<Object[]> r) { return r.stream().map( item -> { return new RevisionListEntity( item[0], Revision.parse((DefaultRevisionEntity) item[1]), (RevisionType) item[2] ); } ).collect(toList()); } |
Интеграция получения данных из истории изменений со стандартным репозиторием модели
Получение данных из истории изменений удобно включит в репозиторий соответствующей сущности.
В нашем случае он представляет собой простейший JPA репозиторий.
1 2 3 |
@Repository public interface GoodsRepository extends JpaRepository<Goods, Long>{ } |
Создадим интерфейс, в котором объявим нужные нам методы.
1 2 3 4 5 |
public interface GoodsHistoryRepository { List<Object[]> getHistoryById(long id); List<Object[]> editionByDate(LocalDate dateTime, long id); Goods editionByRevision(int revisionId, long id); } |
Затем добавим его реализацию. На ней мы останавливаться не станем, т.к. она уже достаточно детально описана ранее
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class GoodsHistoryRepositoryImpl implements GoodsHistoryRepository { @PersistenceContext private EntityManager em; @Override public List<Object[]> getHistoryById(long id) { return AuditReaderFactory.get(em).createQuery() .forRevisionsOfEntity(Goods.class, false, true) .add(AuditEntity.id().eq(id)).getResultList(); } @Override public List<Object[]> editionByDate(LocalDate dateTime, long id) { long endDateTimestamp = dateTime.plusDays(1) .atStartOfDay(ZoneId.systemDefault()).toInstant().getLong(ChronoField.INSTANT_SECONDS) * 1000; long startDateTimestamp = dateTime .atStartOfDay(ZoneId.systemDefault()).toInstant().getLong(ChronoField.INSTANT_SECONDS) * 1000; return AuditReaderFactory.get(em).createQuery() .forRevisionsOfEntity(Goods.class, false, true) .add(AuditEntity.id().eq(id)) .add(AuditEntity.revisionProperty("timestamp").ge(startDateTimestamp)) .add(AuditEntity.revisionProperty("timestamp").lt(endDateTimestamp)) .getResultList(); } @Override public Goods editionByRevision(int revisionId, long id) { AuditQuery query = AuditReaderFactory.get(em).createQuery().forEntitiesAtRevision(Goods.class, revisionId); query.add(AuditEntity.id().eq(id)); return (Goods) query.getSingleResult(); } } |
Далее остаётся только указать репозиторий в качестве наследника реализованного нами интерфейса.
1 2 3 |
@Repository public interface GoodsRepository extends JpaRepository<Goods, Long>, GoodsHistoryRepository { } |
Теперь мы можем получать данные из истории изменений посредством обычного репозитория.
1 |
List<Object[]> data = goodsRepository.editionByDate(LocalDate.of(year, month, day), id); |
Добавить комментарий