2022. 7. 7. 16:19ㆍWeb/Spring
히스토리를 일일히 짜서 쿼리에 넣다가 (이것 때문에 로직이 엄청 복잡해짐.)
하이버네이트에서 제공하는 엔티티 변경 이력을 기록하는 라이브러리가 있다고 해서 사용해보았다.
설정을 하다보니 관련한 자료가 좀 많이 부족했고 ㅠ.. (그래서 동영상도 보고 공식 문서도 찾아보는 등 삽질을 엄청 오래함.)
테스트 코드까지 통과를 했으니 설정했던 부분을 적어놓으려고 한다.
Spring data envers dependency를 추가했고, RevisionEntity Repository를 extends 하여 레포지토리에 지정된 곳에 사용할 수 있었다.
dependencies {
implementation 'org.springframework.data:spring-data-envers'
// 다른 설정 생략
}
현재의 경우에서는 리뷰를 작성할 경우 포인트를 조건에 따라 발생하는 식으로 계산하였고, EventRepository에 extends 하였다.
import com.example.triple.domain.Event;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.history.RevisionRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface EventRepository extends JpaRepository<Event, Long>, RevisionRepository<Event, Long, Long> {
}
RevisionRepository를 따라 들어가보니 아래와 같았다.
package org.springframework.data.repository.history;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.history.Revision;
import org.springframework.data.history.RevisionSort;
import org.springframework.data.history.Revisions;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.Repository;
@NoRepositoryBean
public interface RevisionRepository<T, ID, N extends Number & Comparable<N>> extends Repository<T, ID> {
/**
* Returns the revision of the entity it was last changed in.
*
* @param id must not be {@literal null}.
* @return
*/
Optional<Revision<N, T>> findLastChangeRevision(ID id);
/**
* Returns all {@link Revisions} of an entity with the given id.
*
* @param id must not be {@literal null}.
* @return
*/
Revisions<N, T> findRevisions(ID id);
/**
* Returns a {@link Page} of revisions for the entity with the given id. Note, that it's not guaranteed that
* implementations have to support sorting by all properties.
*
* @param id must not be {@literal null}.
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}.
* @see RevisionSort
* @return
*/
Page<Revision<N, T>> findRevisions(ID id, Pageable pageable);
/**
* Returns the entity with the given ID in the given revision number.
*
* @param id must not be {@literal null}.
* @param revisionNumber must not be {@literal null}.
* @return the {@link Revision} of the entity with the given ID in the given revision number.
* @since 1.12
*/
Optional<Revision<N, T>> findRevision(ID id, N revisionNumber);
}
추가적으로 애플리케이션 위에 @EnableJpaRepositories를 추가해 주었다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.envers.repository.support.EnversRevisionRepositoryFactoryBean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
public class TripleApplication {
public static void main(String[] args) {
SpringApplication.run(TripleApplication.class, args);
}
}
엔티티가 변경될 때 마다 _aud 테이블에 변경된 이력이 하나씩 수정되는데 revision number를 통해 가져오거나 id를 통해 가져올 수 있다. 이를 토대로 테스트 코드를 작성해봤다.
설정한 설계는 아래와 같은데 조건에 따라 포인트를 부여한다.
다른 부분은 생략했고 리뷰와 이벤트 발생 부분을 적었는데 위와 같다.
@Audit를 사용하여 Review, Event와 User에 각각 _aud로 엔티티 변경에 대한 히스토리를 기록하게끔 하였다. 엔티티나 필드에 @Audited를 사용하면 데이터가 변경이 발생할 때에 _aud에 revtype에 따라 히스토리가 기록된다고 한다.
여기서 하나 좀 신경써야 할 부분이 만약 @Audited를 엔티티에 사용한다면 연관한 모든 필드에도 적용되기 때문에 연관관계에 있는 필드도 같이 @Audited를 사용해야 하며, 그렇지 않는 경우에는 @NotAudited를 사용해야 에러가 발생하지 않는다.
ERD를 설계할 때 한 장소에 하나의 리뷰만 사용할 수 있도록 하였고 Review 엔티티 필드에 있는 Place는 굳이 기록할 필요가 없어서 옵션을 추가하였다.
package com.example.triple.domain;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.envers.Audited;
import org.hibernate.envers.RelationTargetAuditMode;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.*;
import static org.hibernate.envers.RelationTargetAuditMode.NOT_AUDITED;
@Entity @Getter @Setter
@NoArgsConstructor
@Audited
@Table(indexes = {@Index(name = "idx__user__place",columnList = "USER_ID")})
public class Review {
@Id @GeneratedValue(generator = "hibernate-uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
@Column(columnDefinition = "BINARY(16)", name = "REVIEW_ID")
private UUID id;
private String contents;
private LocalDateTime dateTime;
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
@ManyToOne
@Audited(targetAuditMode = NOT_AUDITED)
private Place place;
@OneToMany(mappedBy = "review", cascade = CascadeType.ALL)
private List<Photo> photos = new ArrayList<>();
@OneToMany(mappedBy = "review", cascade = CascadeType.ALL)
private List<Event> events = new ArrayList<>();
@Builder
public Review(String contents, Place place, User user, List<Photo> photos) {
this.user = user;
this.contents = contents;
this.place = place;
this.photos = photos;
this.dateTime = LocalDateTime.now();
}
}
이러한 식으로 엔티티 연관관계를 설정하였고 관련하여 생성된 테이블은 아래 그림과 같다.
가령 event를 살펴본다면 revtype(0,1,2)에 따른 기록을 보여준다. 0은 생성, 1은 수정, 2는 삭제다._aud 테이블은 전체 히스토리를 관리하는 revinfo와 조인 관계다.revinfo 테이블은 전체 히스토리를 관리하는데 revinfo 테이블 컬럼을 보면 다음과 같다.
가령 리뷰를 작성하거나 수정하는 경우 rev에 auto_increment로 하나씩 증가하는데 rev 컬럼과 조인이 되어 전체 이력을 fk로 관리하는 것이다.
하지만 원하는 기능은 revision number마다 가져오는게 아니라 조건에 따라 원하는 값을 가져오는 것이었고, Revision 보다 더 간편한 기능이 필요했다. find all revision 키워드로 검색을 했고 stackoverflow에 있는 질문을 보게 되었다.
제일 먼저 했던 부분은 dependency 설정이다. gradle에 한줄을 추가하면 된다.
dependencies {
// 이외 설정은 생략
implementation 'org.hibernate:hibernate-envers'
}
spring data에서 제공하는 envers와 hibernate에서 제공하는 envers가 조금 다르다고 한다.
일단 과제를 먼저 제출해야 하고, erd랑 flowchart도 만들어야 해서 시간이 없는 관계로 추가적으로 작성해야 겠다.
참고
http://useof.org/java-open-source/org.hibernate.envers.AuditReader
https://developer.jboss.org/thread/199785
https://www.baeldung.com/database-auditing-jpa
'Web > Spring' 카테고리의 다른 글
[Spring] Spring Security 기본 이해 - (5) (0) | 2022.07.13 |
---|---|
[Spring] Spring Security 기본 이해 - (4) (0) | 2022.07.10 |
[JPA] Hibernate Envers 오류 : Such mapping is possible, but has to be explicitly defined using @Audited (0) | 2022.07.04 |
[Spring] Spring Security 기본 이해 - (3) (0) | 2022.06.27 |
[Spring] QueryDSL 오류 - org.springframework.dao.InvalidDataAccessApiUsageException: No sources given (0) | 2022.06.23 |