Команда Spring АйО перевела статью, которая отлично подойдёт тем, кто ещё не знаком со Spring Data Envers. В статье на простых примерах объясняется, как отслеживать изменения данных в приложении, используя этот инструмент.
Введение
В этой статье мы рассмотрим проект Spring Data Envers и разберемся, как извлечь из него максимум пользы.
Hibernate Envers — это расширение Hibernate ORM, которое позволяет отслеживать изменения сущностей с минимальными изменениями на уровне приложения.
Так же, как Envers интегрируется с Hibernate ORM для ведения журнала изменений сущностей, проект Spring Data Envers подключается к Spring Data JPA, чтобы добавить возможность ведения журнала изменений с использованием JPA репозиториев.
Доменная модель
Предположим, у нас есть сущность Post
, которая отмечена аннотацией @Audited
из проекта Hibernate Envers:
@Entity @Table(name = "post", uniqueConstraints = @UniqueConstraint( name = "UK_POST_SLUG", columnNames = "slug" ) ) @Audited public class Post { ⠀ @Id @GeneratedValue private Long id; ⠀ @Column(length = 100) private String title; ⠀ @NaturalId @Column(length = 75) private String slug; ⠀ @Enumerated(EnumType.ORDINAL) @Column(columnDefinition = "NUMERIC(2)") private PostStatus status; }
Сущность Post
имеет дочернюю сущность PostComment
, которая также аннотирована @Audited
:
@Entity @Table(name = "post_comment") @Audited public class PostComment { ⠀ @Id @GeneratedValue private Long id; ⠀ @Column(length = 250) private String review; ⠀ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(foreignKey = @ForeignKey(name = "FK_POST_COMMENT_POST_ID")) private Post post; }
Скрытый текст
Как я объяснял в этой статье, мы будем использовать стратегию ValidityAuditStrategy
, так как она может ускорить выполнение запросов связанных с журналом изменений.
Чтобы включить стратегию ValidityAuditStrategy
, необходимо установить следующее свойство Hibernate-конфигурации:
properties.setProperty( EnversSettings.AUDIT_STRATEGY, ValidityAuditStrategy.class.getName() );
При генерации схемы с использованием инструмента hbm2ddl, Hibernate создаст следующие таблицы в базе данных:
Каждый раз, когда транзакция завершается, создается ревизия, которая сохраняется в таблице revinfo
.
Таблица post_aud
отслеживает изменения записей в таблице post
, а таблица post_comment_aud
хранит информацию о журнале изменений для таблицы post_comment
.
Spring Data Envers Репозитории
Проект Spring Data Envers предоставляет интерфейс RevisionRepository, который ваши JPA-репозитории могут расширять, чтобы добавить возможность выполнения запросов связанных с журналом изменений.
Например, репозиторий PostRepository
расширяет JpaRepository
из Spring Data JPA и RevisionRepository
из Spring Data Envers:
@Repository public interface PostRepository extends JpaRepository<Post, Long>, RevisionRepository<Post, Long, Long> { }
Точно так же PostCommentRepository
расширяет как JpaRepository
, так и RevisionRepository
из Spring Data Envers:
@Repository public interface PostCommentRepository extends JpaRepository<PostComment, Long>, RevisionRepository<PostComment, Long, Long> { void deleteByPost(Post post); }
На сервисном слое у нас есть класс PostService
, который предоставляет методы для сохранения и удаления сущностей Post
и PostComment
. Эти методы помогут нам увидеть, как работает механизм ведения журнала изменений:
@Transactional(readOnly = true) public class PostService { @Autowired private PostRepository postRepository; @Autowired private PostCommentRepository postCommentRepository; @Transactional public Post savePost(Post post) { return postRepository.save(post); } @Transactional public Post savePostAndComments(Post post, PostComment... comments) { post = postRepository.save(post); if (comments.length > 0) { postCommentRepository.saveAll(Arrays.asList(comments)); } return post; } @Transactional public void deletePost(Post post) { postCommentRepository.deleteByPost(post); postRepository.delete(post); } }
Отслеживание операций INSERT, UPDATE и DELETE
При создании родительской сущности Post
вместе с двумя дочерними сущностями PostComment
:
Post post = new Post() .setTitle("High-Performance Java Persistence 1st edition") .setSlug("high-performance-java-persistence") .setStatus(PostStatus.APPROVED); postService.savePostAndComments( post, new PostComment() .setPost(post) .setReview("A must-read for every Java developer!"), new PostComment() .setPost(post) .setReview("Best book on JPA and Hibernate!") );
Hibernate сгенерирует следующие SQL-запросы:
SELECT nextval('post_SEQ') SELECT nextval('post_comment_SEQ') SELECT nextval('post_comment_SEQ') INSERT INTO post (slug, status, title, id) VALUES ( 'high-performance-java-persistence', 1, 'High-Performance Java Persistence 1st edition', 1 ) INSERT INTO post_comment (post_id, review, id) VALUES ( 1, 'A must-read for every Java developer!', 1 ), ( 1, 'Best book on JPA and Hibernate!', 2 ) SELECT nextval('REVINFO_SEQ') INSERT INTO REVINFO (REVTSTMP, REV) VALUES (1726724588078, 1) INSERT INTO post_AUD (REVEND, REVTYPE, slug, status, title, REV, id) VALUES ( null, 0, 'high-performance-java-persistence', 1, 'High-Performance Java Persistence 1st edition', 1, 1 ) INSERT INTO post_comment_AUD (REVEND, REVTYPE, post_id, review, REV, id) VALUES ( null, 0, 1, 'A must-read for every Java developer!', 1, 1 ), ( null, 0, 1, 'Best book on JPA and Hibernate!', 1, 2 )
В то время как Hibernate ORM выполняет INSERT-запросы для записей в таблицах post
и post_comment
, Hibernate Envers создает записи в таблицах REVINFO
, post_AUD
и post_comment_AUD
.
При изменении сущности Post
:
post.setTitle("High-Performance Java Persistence 2nd edition"); postService.savePost(post);
Hibernate сгенерирует следующие запросы:
SELECT p1_0.id, p1_0.slug, p1_0.status, p1_0.title FROM post p1_0 WHERE p1_0.id = 1 UPDATE post SET status = 1, title = 'High-Performance Java Persistence 2nd edition' WHERE id = 1 SELECT nextval('REVINFO_SEQ') INSERT INTO REVINFO (REVTSTMP, REV) VALUES (1726724799884, 2) INSERT INTO post_AUD (REVEND, REVTYPE, slug, status, title, REV, id) VALUES ( null, 1, 'high-performance-java-persistence', 1, 'High-Performance Java Persistence 2nd edition', 2, 1 ) UPDATE post_AUD SET REVEND = 2 WHERE id = 1 AND REV <> 2 AND REVEND IS NULL
Обратите внимание, что была создана новая запись в REVINFO
, которая связана с записью в post_AUD
.
А при удалении сущности Post
:
postService.deletePost(post);
Hibernate выполнит следующие запросы:
SELECT pc1_0.id, pc1_0.post_id, pc1_0.review FROM post_comment pc1_0 WHERE pc1_0.post_id = 1 SELECT p1_0.id,p1_0.slug,p1_0.status,p1_0.title FROM post p1_0 WHERE p1_0.id = 1 DELETE FROM post_comment WHERE id = 1 DELETE FROM post_comment WHERE id = 2 DELETE FROM post WHERE id = 1 INSERT INTO REVINFO (REVTSTMP, REV) VALUES (1726724982890, 3) INSERT INTO post_comment_AUD (REVEND, REVTYPE, post_id, review, REV, id) VALUES ( null, 2, null, null, 3, 1 ), ( null, 2, null, null, 3, 2 ) INSERT INTO post_AUD (REVEND, REVTYPE, slug, status, title, REV, id) VALUES ( null, 2, null, null, null, 3, 1 ) UPDATE post_comment_AUD SET REVEND = 3 WHERE id = 1 AND REV <> 3 AND REVEND IS NULL UPDATE post_comment_AUD SET REVEND = 3 WHERE id = 2 AND REV <> 3 AND REVEND IS NULL UPDATE post_AUD SET REVEND = 3 WHERE id = 1 AND REV <> 3 AND REVEND IS NULL
Загрузка ревизий с использованием Spring Data Envers
Интерфейс RevisionRepository
из Spring Data Envers предоставляет несколько методов для загрузки ревизий сущностей.
Например, если вы хотите загрузить последнюю ревизию сущности Post
, можно использовать метод findLastChangeRevision
, который унаследован от RevisionRepository
:
Revision<Long, Post> latestRevision = postRepository.findLastChangeRevision(post.getId()) .orElseThrow(); LOGGER.info("The latest Post entity operation was [{}] at revision [{}]", latestRevision.getMetadata() .getRevisionType(), latestRevision.getRevisionNumber() .orElseThrow());
При запуске примера вы увидите следующее сообщение в логах:
The latest Post entity operation was [DELETE] at revision [3]
Чтобы загрузить все ревизии для сущности, можно использовать метод findRevisions
, который также унаследован от RevisionRepository
:
for (Revision<Long, Post> revision : postRepository.findRevisions(post.getId())) { LOGGER.info( "At revision [{}], the Post entity state was: [{}]", revision.getRevisionNumber().orElseThrow(), revision.getEntity() ); }
При запуске этого кода в логах появятся следующие записи:
At revision [1], the Post entity state was: [ { id = 1, title = 'High-Performance Java Persistence 1st edition', slug = 'high-performance-java-persistence', status = APPROVED } ] At revision [2], the Post entity state was: [ { id = 1, title = 'High-Performance Java Persistence 2nd edition', slug = 'high-performance-java-persistence', status = APPROVED } ] At revision [3], the Post entity state was: [ { id = 1, title = null, slug = null, status = null } ]
Загрузка ревизий с использованием постраничной выборки
Предположим, мы создали несколько ревизий для сущности Post
:
Post post = new Post() .setTitle("Hypersistence Optimizer, version 1.0.0") .setSlug("hypersistence-optimizer") .setStatus(PostStatus.APPROVED); postService.savePost(post); for (int i = 1; i < 20; i++) { post.setTitle(String.format( "Hypersistence Optimizer, version 1.%d.%d", i / 10, i % 10) ); postService.savePost(post); }
Мы можем загружать ревизии с постраничной выборкой с помощью метода findRevisions(ID id, Pageable pageable)
.
Например, чтобы получить первую страницу с ревизиями в порядке убывания, можно использовать PageRequest
, как показано в следующем примере:
int pageSize = 10; Page<Revision<Long, Post>> firstPage = postRepository.findRevisions( post.getId(), PageRequest.of(0, pageSize, RevisionSort.desc()) ); logPage(firstPage);
При запуске этого кода для первой страницы мы увидим следующие ревизии:
Скрытый текст
At revision [23], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.9', slug='hypersistence-optimizer', status=APPROVED } ] At revision [22], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.8', slug='hypersistence-optimizer', status=APPROVED } ] At revision [21], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.7', slug='hypersistence-optimizer', status=APPROVED } ] At revision [20], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.6', slug='hypersistence-optimizer', status=APPROVED } ] At revision [19], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.5', slug='hypersistence-optimizer', status=APPROVED } ] At revision [18], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.4', slug='hypersistence-optimizer', status=APPROVED } ] At revision [17], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.3', slug='hypersistence-optimizer', status=APPROVED } ] At revision [16], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.2', slug='hypersistence-optimizer', status=APPROVED } ] At revision [15], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.1', slug='hypersistence-optimizer', status=APPROVED } ] At revision [14], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.1.0', slug='hypersistence-optimizer', status=APPROVED } ]
При логировании ревизий, полученных для второй страницы:
Page<Revision<Long, Post>> secondPage = postRepository.findRevisions( post.getId(), PageRequest.of(1, pageSize, RevisionSort.desc()) ); logPage(secondPage);
В логах появятся следующие записи:
Скрытый текст
At revision [13], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.9', slug='hypersistence-optimizer', status=APPROVED } ] At revision [12], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.8', slug='hypersistence-optimizer', status=APPROVED } ] At revision [11], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.7', slug='hypersistence-optimizer', status=APPROVED } ] At revision [10], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.6', slug='hypersistence-optimizer', status=APPROVED } ] At revision [09], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.5', slug='hypersistence-optimizer', status=APPROVED } ] At revision [08], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.4', slug='hypersistence-optimizer', status=APPROVED } ] At revision [07], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.3', slug='hypersistence-optimizer', status=APPROVED } ] At revision [06], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.2', slug='hypersistence-optimizer', status=APPROVED } ] At revision [05], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.1', slug='hypersistence-optimizer', status=APPROVED } ] At revision [04], the Post entity state was: [ Post{id=2, title='Hypersistence Optimizer, version 1.0.0', slug='hypersistence-optimizer', status=APPROVED } ]
Здорово, правда?
Заключение
Хотя существует множество CDC (Change Data Capture) решений для отслеживания изменений сущностей, Envers, вероятно, является самым простым вариантом, если вы уже используете Hibernate ORM.
А если вы используете Spring Data JPA, то с помощью Spring Data Envers можно добавить в ваши репозитории возможность работы с ревизиями.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
Ждем всех, присоединяйтесь
ссылка на оригинал статьи https://habr.com/ru/articles/849086/
Добавить комментарий