졸업 프로젝트를 진행하면서 엔티티의 변경 이력을 관리하는 기능을 구현하게 되었다.
처음에는 전에 구현한 것과 동일한 방식으로 변경 이력을 기록하는 history 테이블을 하나 더 만들어 구현하려고 했다. 그러나 새로운 엔티티를 작성하고, 변경 이력 관리 로직을 짜던 중 갑자기 굉장히 번거롭고 귀찮게 느껴졌다. 그때는 하나의 테이블에 대해서만 변경 이력을 관리하면 됐기 때문에 테이블 한 개만 추가하면 됐지만, 이번 프로젝트에서는 무려 8개나..^.^ 만들어야 했다.
하라면 할 수 있겠지만, 나에게 시간은 별로 없고... 무엇보다 효율적으로 작업하고 싶었다. 그때 머릿속에서 스쳐지나간 생각이 있으니, 바로 JPA Auditing이었다. 생성 시간과 수정 시간도 관리해주는데, 분명히 변경 이력을 관리해주는 라이브러리도 있을 거라며 구글링 해봤는데 역시나! 변경 이력을 관리해주는 스프링 데이터 envers 라이브러리가 있었다. 개꿀. 전세계 개발자님들 저는 당신들을 믿고 있었어요.
그렇게 진행 중인 프로젝트에 스프링 데이터 envers를 적용하게 되었다. 프로젝트 규모와 달리 꽤 복잡한 검색 조건을 걸어야 했기에 정확히는 하이버네이트 envers를 적용했고, 아래 내용은 이를 위해 학습한 내용을 정리한 글이다.
- 하이버네이트가 제공하는 핵심 모듈
- JPA 스펙에 정의된 모든 매핑 감사
- 엔티티의 변경 이력을 자동으로 관리
implementation 'org.hibernate:hibernate-envers:${version}'
@Audited
어노테이션을 사용한다.@NotAudited
어노테이션을 사용한다.@Entity
@Audited
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private String tel;
...
}
그러면 아래와 같은 MEMBER_AUD
테이블이 생성된다.
CREATE TABLE MEMBER_AUD (
id BIGINT NOT NULL,
rev INTEGER NOT NULL,
revtype TINYINT,
name VARCHAR(255),
tel VARCHAR(255),
PRIMARY KEY(id, rev)
);
과거 특정 시점의 Revision을 조회할 수 있다. 단건 조회, 리스트 조회 모두 가능하며, 검색 조건과 페이징도 지원된다.
public List<Member> findRevisions(Long id) {
return auditReader().createQuery()
.forRevisionsOfEntity(Member.class, true, true)
.add(AuditEntity.id().eq(id))
.getResultList();
}
public List<Member> findRevisionsWithWhere() {
return auditReader().createQuery()
.forRevisionsOfEntity(Member.class, true, true)
// 이름이 무명인 데이터 조회
.add(AuditEntity.property("name").eq("무명"))
// 수정 이력만 조회
.add(AuditEntity.revisionType().eq(RevisionType.MOD))
// 페이징 조건
.setFirstResult(0)
// 페이징 조건
.setMaxResults(2)
// 정렬 조건
.addOrder(AuditEntity.property("tel").desc())
.getResultList();
}
forRevisionsOfEntity()
의 두 번째 파라미터를 false
로 바꾸면 리턴 타입이 Object[]로 바뀐다.public List<Object[]> findRevisions(Long id) {
return auditReader().createQuery()
// 두 번째 파라미터를 false로 바꿈
.forRevisionsOfEntity(Member.class, false, true)
.add(AuditEntity.id().eq(id))
.getResultList();
}
allRevision[0] = Member{id=1, name='무명', tel='010-1111'}
allRevision[1] = DefaultRevisionEntity(id=1, revisionDate=Aug 20, 2023 10:39:14 PM)
allRevision[2] = ADD
allRevision[0] = Member{id=1, name='무명', tel='010-2222'}
allRevision[1] = DefaultRevisionEntity(id=2, revisionDate=Aug 20, 2023 10:49:14 PM)
allRevision[2] = MOD
어떤 필드를 수정했는지 알고 싶거나 필드의 수정 여부를 검색 조건으로 사용하고 싶으면 @Audited(withModifiedFlag=true)
와 같이 withModifiedFlag=true
속성을 지정해준다. 그러면 AUD 테이블에 수정 상태 컬럼이 추가된다.
ex) NAME -> NAME_MOD, TEL -> TEL_MODTRUE면 수정된 상태이고, FALSE면 수정되지 않은 상태이다.
JPA가 히스토리 테이블을 아래와 같이 생성해준다.CREATE TABLE MEMBER_AUD ( id BIGINT NOT NULL, rev INTEGER NOT NULL, revtype TINYINT, name VARCHAR(255), name_mod BOOLEAN, tel VARCHAR(255), tel_mod BOOLEAN, CONSTRAINT CONSTRAINT_C PRIMARY KEY(id, rev) );
public List<Member> findRevisionsWithChanged(Long id) {
return auditReader().createQuery()
.forRevisionsOfEntity(Member.class, true, true)
.add(AuditEntity.id().eq(id))
// 이름이 수정되지 않은 데이터 조회
.add(AuditEntity.property("name").hasNotChanged())
// 연락처가 수정된 데이터 조회
.add(AuditEntity.property("tel").hasChanged())
.getResultList();
}
// 연관관계가 있는 엔티티를 감사하지 않음.
// 연관관계 추적 테이블 생성은 됨.
@Audited(targetAuditMode=NOT_AUDITED)
// 상속 관계에 있을 때, 부모에 있는 속성까지 히스토리 관리
// 아래 예시에서는 BaseEntity 클래스까지 히스토리가 관리됨.
@AuditOverride(forClass=BaseEntity.class)
- 스프링 데이터 JPA의 확장 모듈
- 하이버네이트 Envers를 편리하게(스프링 데이터스럽게) 조회하도록 도움
- RevisionRepository 인터페이스 제공
- 편리한 메타데이터 조회
// hibernate-envers 자동 포함됨
implementation 'org.springframework.data:spring-data-envers:${version}'
@EnableJpaRepositories(repositoryFactoryBeansClass
=EnversRevisionRepositoryFactoryBean.class)
@NoRepositoryBean
public interface RevisionRepository extends Repository<T, ID> {
// 최근 리비전 조회 (현 데이터에 근접)
Revision<N, T> findLastChangeRevision(ID id);
// 모든 히스토리 조회
Revision<N, T> findRevisions(ID id);
// 히스토리 페이징 + 정렬 조회
Page<Revision<N, T>> findRevisions(ID id, Pageable pageable);
// 특정 리비전 조회
Revision<N, T> findRevision(ID id, N revisionNumber);
}
RevisionRepository<Entity, PK의 타입, REV의 타입>
public interface MemberRepository extends JpaRepository<Member, Long>,
RevisionRepository<Member, Long, Integer> {
}
Revision<Integer, Member> revision
= memberRepository.findRevision(member.getId(), 1);
Member entity = revision.getEntity(); // 엔티티
Integer revisionNumber = revision.getRevisionNumber(); // 리비전
DateTime dateTime = revision.getRevisionDate(); // 변경 날짜
Page<Revision<Integer, Member>> result
= memberRepository.findRevisions(member.getId(),
new PageRequest(0, 10, RevisionSort.desc()));
result.getTotalElements(); // 전체 수
result.getContent(); // 내용
스프링캠프 2017 [Day2 A5] : 엔티티 히스토리를 편리하게 관리해주는 스프링 데이터 Envers
spring data envers 로 데이터 변경 로깅하기