개요
Envers 모듈은 Hibernate 및 JPA와 함께 작동하는 핵심 Hibernate 모델입니다. 사실 독립형이든, WildFly이든 JBoss AS, Spring, Grails 등이든 Hibernate가 작동하는 모든 곳에서 Envers를 사용할 수 있습니다.
Envers 모듈은 엔티티 클래스를위한 쉬운 감사 / 버전 관리 솔루션을 제공하는 것을 목표로합니다.
- 하이버네이트 핵심 모듈
- JPA 스펙에 정의된 모든 매핑 감사
- 엔티티의 변경 이력을 자동 관리
- 트랜잭션 단위의 통합 Revision 관리 (Snapshot)
- REVINFO 테이블은 revision_id 와 Timestamp만 가지고 있고, 이력 테이블은 별도로 존재함. 한 트랜잭션 내에서 발생한 변경사항을 revision_id 1이라는 이력으로 여러 이력테이블에 각각 저장, revision_id 1인 항목의 해당 트랜잭션에서 변경된 모든 이력을 감사(audit)가능함
빠른 시작
- 프로젝트에
hibernate-envers
종속성 추가 - 이력관리가 필요한 엔티티 또는 프로퍼티에
@Audited
추가 - JpaRepository 상속한 인퍼테이스에
RevisionRepository<T, ID, N>
상속하여 사용 - RevisionRepository 사용을 위한 설정 추가
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) // 설정 추가
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
예시
@Entity
public class Person {
@Id
@GeneratedValue
private Integer id;
@Audited
private String name;
@Audited
private String surname;
@Audited
@ManyToOne
private Address address;
}
Enver 엔티티 맵핑
hibernate.hbm2ddl.auto
옵션이create
,create-drop
,update
면 audit 테이블이 자동생성됨
@Audited
@Entity(name = "Customer")
public static class Customer {
@Id
private Long id;
private String firstName;
private String lastName;
@Temporal( TemporalType.TIMESTAMP )
@Column(name = "created_on")
@CreationTimestamp
private Date createdOn;
//Getters, setters 생략
}
생성된 SQL
create table Customer (
id bigint not null,
created_on timestamp,
firstName varchar(255),
lastName varchar(255),
primary key (id)
)
create table Customer_AUD (
id bigint not null,
REV integer not null,
REVTYPE tinyint,
created_on timestamp,
firstName varchar(255),
lastName varchar(255),
primary key (id, REV)
)
create table REVINFO (
REV integer generated by default as identity,
REVTSTMP bigint,
primary key (REV)
)
alter table Customer_AUD
add constraint FK5ecvi1a0ykunrriib7j28vpdj
foreign key (REV)
references REVINFO
REVTYPE 이란?
RevisionType Enum
- 0: (ADD) 행 추가
- 1: (MOD) 행 수정
- 2: (DEL) 행 삭제
Insert 해보기
- 코드
Customer customer = new Customer();
customer.setId( 1L );
customer.setFirstName( "존" );
customer.setLastName( "찰스" );
entityManager.persist( customer );
- 결과
insert
into
Customer
(created_on, firstName, lastName, id)
values
(?, ?, ?, ?)
-- binding parameter [1] as [TIMESTAMP] - [THU Aug 06 17:21:32 EEST 2020]
-- binding parameter [2] as [VARCHAR] - [존]
-- binding parameter [3] as [VARCHAR] - [찰스]
-- binding parameter [4] as [BIGINT] - [1]
insert
into
REVINFO
(REV, REVTSTMP)
values
(?, ?)
-- binding parameter [1] as [BIGINT] - [1]
-- binding parameter [2] as [BIGINT] - [1500906092803]
insert
into
Customer_AUD
(REVTYPE, created_on, firstName, lastName, id, REV)
values
(?, ?, ?, ?, ?, ?)
-- binding parameter [1] as [INTEGER] - [0]
-- binding parameter [2] as [TIMESTAMP] - [THU Aug 06 17:21:32 EEST 2020]
-- binding parameter [3] as [VARCHAR] - [존]
-- binding parameter [4] as [VARCHAR] - [찰스]
-- binding parameter [5] as [BIGINT] - [1]
-- binding parameter [6] as [INTEGER] - [1]
Update 해보기
- 코드
Customer customer = entityManager.find( Customer.class, 1L );
customer.setLastName( "존 Jr." );
- 결과
update
Customer
set
created_on=?,
firstName=?,
lastName=?
where
id=?
-- binding parameter [1] as [TIMESTAMP] - [2020-08-06 17:21:32.757]
-- binding parameter [2] as [VARCHAR] - [존]
-- binding parameter [3] as [VARCHAR] - [존 Jr.]
-- binding parameter [4] as [BIGINT] - [1]
insert
into
REVINFO
(REV, REVTSTMP)
values
(?, ?)
-- binding parameter [1] as [BIGINT] - [2]
-- binding parameter [2] as [BIGINT] - [1500906092853]
insert
into
Customer_AUD
(REVTYPE, created_on, firstName, lastName, id, REV)
values
(?, ?, ?, ?, ?, ?)
-- binding parameter [1] as [INTEGER] - [1]
-- binding parameter [2] as [TIMESTAMP] - [2020-08-06 17:21:32.757]
-- binding parameter [3] as [VARCHAR] - [존]
-- binding parameter [4] as [VARCHAR] - [존 Jr.]
-- binding parameter [5] as [BIGINT] - [1]
-- binding parameter [6] as [INTEGER] - [2]
Delete 해보기
- 코드
Customer customer = entityManager.getReference( Customer.class, 1L );
entityManager.remove( customer );
- 결과
delete
from
Customer
where
id = ?
-- binding parameter [1] as [BIGINT] - [1]
insert
into
REVINFO
(REV, REVTSTMP)
values
(?, ?)
-- binding parameter [1] as [BIGINT] - [3]
-- binding parameter [2] as [BIGINT] - [1500906092876]
insert
into
Customer_AUD
(REVTYPE, created_on, firstName, lastName, id, REV)
values
(?, ?, ?, ?, ?, ?)
-- binding parameter [1] as [INTEGER] - [2]
-- binding parameter [2] as [TIMESTAMP] - [null]
-- binding parameter [3] as [VARCHAR] - [null]
-- binding parameter [4] as [VARCHAR] - [null]
-- binding parameter [5] as [BIGINT] - [1]
-- binding parameter [6] as [INTEGER] - [3]
@OneToMany + @JoinColumn 이력관리
OneToMany나 JoinColumn으로 설정된 연관관계의 주인이 아닌 엔티티 컬렉션이 바뀔 때도@AuditMappedBy를 사용하여 이력을 관리할 수 있습니다.
RevisionRepository 인터페이스로 이력 조회하기
Spring data envers라는 스프링 데이터 JPA의 확장 모듈
- 하이버네이트 Envers를 편리하게 조회하도록 도움
- build.gradle 파일에
implementation 'org.springframework.data:spring-data-envers'
라이브러리 의존성 추가 - 단점 : 버전업이 잘 안되며 기능이 단순한 편,
스프링 데이터가 지원하는 Querydsl 관련 기능과 함께 사용하려면 코드를 약간 수정해야 한다.(수정됨)
이력관리 조회는 JpaRepository
처럼 RevisionRepository
인터페이스를 상속하여 사용하면 됩니다.
@NoRepositoryBean
public interface RevisionRepository extends Repository<T, ID> {
//최근 리비전 조회
Revision<N, T> findLastChangeRevision(ID id);
//id를 사용하여 해당 id의 모든 리비전 조회
Revisions<N, T> findRevisions(ID id);
//리비전을 페이징 처리하여 조회
Page<Revision<N, T>> findRevisions(ID id, Pageable pageable);
//특정 리비전 조회
Revision<N, T> findRevision(ID id, N revisionNumber);
}
EnversRevisionRepository 사용하기 위한 설정
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
CustomerRepository 만들기
public interface CustomerRepository extends JpaRepository<Customer, Long>, RevisionRepository<Customer, Long, Integer> {
}
- 단순 조회
Revision<Long, IotManagementDevice> revision = repository.findRevision(iotManagementDevice.getId(), 1);
IotManagementDevice entity = revision.getEntity(); // 엔티티
Long revisionNumber = revision.getRevisionNumber(); // 리비전
DateTime dateTime = revision.getRevisionDate(); // 변경 날짜
- 페이징, 정렬조회
Page<Revision<Long, IotManagementDevice>> result = repository.findRevisions(iotManagementDevice.getId(), new PageRequest(**0, 10**, **RevisionSort.desc()**));
result.**getTotalElements**(); // 전체 수
result.**getContent**(); // 내용
AuditReader 인터페이스로 이력 조회하기
Hibernate가 지원하는 native 이력조회 인터페이스
복잡한 조건을 포함하여 이력을 검색하거나 페이징이 필요한 경우 AuditReader를 사용하여 QueryDSL과 유사한 방식으로 조회할 수 있습니다.
- 과거 특정시점의 Revision을 조회
- 단건 조회
- 리스트 조회
public List<IotManagementDevice> findRevisions(String id) {
return auditReader.createQuery()
.forRevisionsOfEntity(IotManagementDevice.class, true, false)
.add(AuditEntity.id().eq(id))
.getResultList();
}
- 검색 조건 조회
public List<IotManagementDevice> findRevisionsWithWhere() {
int pageSize = 10;
AuditReader auditReader = AuditReaderFactory.get(getEntityManager());
return auditReader.createQuery()
.forRevisionsOfEntity(IotManagementDevice.class, true, true)
// 설치일자 검색
.add(AuditEntity.property("instl_de").eq("2020-06-01"))
// 수정 항목만 검색
.add(AuditEntity.revisionType().eq(RevisionType.MOD))
// 정렬 조건
.addOrder(AuditEntity.revisionNumber().desc())
.getResultList();
}
- 페이징
public List<IotManagementDevice> findRevisionsWithWhere() {
int pageSize = 10;
AuditReader auditReader = AuditReaderFactory.get(getEntityManager());
return auditReader.createQuery()
.forRevisionsOfEntity(IotManagementDevice.class, true, true)
// 페이징 조건
.setFirstResult(0)
.setMaxResults(pageSize)
.getResultList();
}
- 리비전 메타 데이터와 함께 이력 조회
@Builder
@Getter
@Setter
public class IotManagementDeviceAuditDto {
IotManagementDevice iotManagementDevice;
CustomRevisionEntity revisionEntity;
RevisionType revisionType;
}
public List<IotManagementDeviceAuditDto> findRevisionsWithMetadata(String id) {
AuditReader auditReader = AuditReaderFactory.get(getEntityManager());
AuditQuery auditQuery = auditReader.createQuery()
// 2번째 파라미터를 false로 설정 시 반환타입이 Object 배열로 바뀌고, Revision number와 revision type이 같이 반환됨
.forRevisionsOfEntity(IotManagementDevice.class, false, true)
.add(AuditEntity.id().eq(id))
.addOrder(AuditEntity.revisionNumber().desc());
// Object 배열의 List로 넘어오기 때문엥 convert가 필요함 ;;
List<Object[]> revisions = auditQuery.getResultList();
return revisions.stream()
.map(rev -> IotManagementDeviceAuditDto.builder()
.iotManagementDevice((IotManagementDevice) rev[0]) // 0번째는 저장된 이력 엔티티
.revisionEntity((CustomRevisionEntity) rev[1]) // 1번째는 revision 정보
.revisionType((RevisionType) rev[2]).build()) // 2번째는 구분(생성, 수정, 삭제)
.collect(Collectors.toList());
}
Property 설정
Envers Configuration Property 문서
스키마 이름 바꾸기
org.hibernate.envers.default_schema
(default:null
)@AuditTable( schema="…" )
이력테이블 접두어, 접미어
org.hibernate.envers.audit_table_prefix
org.hibernate.envers.audit_table_suffix
(default:_AUD
)
이력테이블 컬럼 이름
org.hibernate.envers.revision_field_name
(default:REV
)org.hibernate.envers.revision_type_field_name
(default:REVTYPE
)
주의⚠
The use of JPA’s CriteriaUpdate and CriteriaDelete bulk operations are not currently supported by Envers due to how an entity’s lifecycle events are dispatched. Such operations should be avoided as they’re not captured by Envers and leads to incomplete audit history.
고려해야 할 점
- 이력이 대용량일 때는 차라리 log를 쌓고 Hadoop과 같은 시스템에서 던져서 검색하는 방식으로 사용하는게 낫다
참고
Hibernate Enver 적용 블로그 (2019.03)
Springboot + Envers로 엔티티 이력 관리하기 2017.09