개요
Hibernate Envers 프로젝트는 각각의 대상 엔티티의 이력관리를 간편하게 도와줍니다
-
-
Hibernate/JPA Entity의 변경 사항을 추적할 수 있게 별도 테이블에 변경 사항을 자동 저장해준다.
-
-
-
Hibernate Envers 의 변경 사항을 쉽게 조회할 수 있게 해준다.
-
Spring Data Envers 적용하기
1. dependency 추가
Gradle (build.gradle)
dependencies {
// envers
implementation "org.springframework.data:spring-data-envers"
}
Maven(pom.xml)
<!-- envers -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
</dependency>
2. EnableJpaRepositories 추가
@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Spring Boot 기준 main 메서드의 클래스에 @EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) 어노테이션을 추가해줍니다.
3. @Audited
@Table(name = "user_info")
@Entity
@Audited
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(columnDefinition = "varchar(100) comment '로그인 아이디'",unique = true)
private String loginId;
@Column(columnDefinition = "varchar(255) comment '이메일 '",unique = true)
private String email;
// ....
}
Entity 위에 @Audited 어노테이션을 추가하면 Entity 의 모든 필드를 추적하게되고,
각 필드에 @Audited 어노테이션을 추가하면 해당 필드만 추적합니다.
추적하고 싶지 않은 필드는?
@NotAudited 를 사용하여 likeGuides 필드는 추적되지 않도록 하면 해결할 수 있습니다.
@NotAudited
@OneToMany(mappedBy = "guide", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<LikeGuide> likeGuides = new ArrayList<>();
BaseEntity와 같이 사용하는 경우에는?
// 상속 관계에 있을 때, 부모에 있는 속성까지 히스토리 관리
// 아래 예시에서는 BaseEntity 클래스까지 히스토리가 관리됨.
@AuditOverride(forClass=BaseEntity.class)
필드 변경여부 관리
@Audited(withModifiedFlag = true)
withModifiedFlag 를 true 로 설정합니다. (기본값 false)
여기서 중요한점은 Transaction 단위로 revision을 관리한다는 것입니다.
아주 큰 장점으로 생각되는게, 한 Transaction에서 변경된 내용을 한번에 파악이 가능하기 때문입니다.
user_info 테이블

user_info_history 테이블

revision_info 테이블

4. application.yaml
spring:
jpa:
properties.org.hibernate.envers:
audit_table_suffix: _history
revision_field_name: revision_id
revision_type_field_name: revision_type
# 해당 transaction 변경이력 있는 데이터 불러오기
# track_entities_changed_in_revision: true
5. Revision & History Table
- Rev : auto_increment와 같은 이력관리 ID값입니다.
- RevType
- 0: 등록
- 1: 수정
- 2: 삭제
jpa 의 테이블 자동생성 기능 으로 테이블을 로컬에서 생성후, 부가적인 정보를 수정한 후 sql client tool 에서 table 을 생성하면 간편하게 히스토리 테이블을 생성할 수 있습니다.
CREATE TABLE revision_info (
revision_id bigint NOT NULL auto_increment,
revtstmp bigint,
PRIMARY KEY (revision_id)
) engine=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE user_info_history (
id bigint NOT NULL,
revision_id bigint not null,
revision_type tinyint,
email varchar(100) comment '이메일 ',
gender varchar(50) null comment '성별',
login_id varchar(100) comment '로그인 아이디',
created_at datetime default current_timestamp comment '등록일시',
updated_at datetime default current_timestamp comment '수정일시',
primary key (id, rev_id)
) engine=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci comment '사용자 History';
ALTER TABLE user_info_history
ADD CONSTRAINT user_info_history_revinfo_fkey
FOREIGN KEY (rev_id) REFERENCES revinfo (rev);

6. Revision Entity
Rev Long으로 변환하기
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Table(name = "revision_info")
@RevisionEntity
@Entity
public class CustomRevisionEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@RevisionNumber
@EqualsAndHashCode.Include
@Column(name = "revision_id")
private long id;
@EqualsAndHashCode.Include
@RevisionTimestamp
@Column(name = "revtstmp")
private long timestamp;
@Transient
public Date getRevisionDate() {
return new Date( timestamp );
}
@Override
public String toString() {
return String.format("CustomRevisionEntity(id = %d, revisionDate = %s)",
id, DateFormat.getDateTimeInstance().format(getRevisionDate()));
}
}
rev는 기본적으로 Integer를 사용합니다.
하지만 rev는 DB 트랜잭션 단위로 증가하기 때문에 여러 테이블이 함께 사용하면 금방 소모되어 Integer.MAX에 다다를 수 있습니다.
따라서 REVINFO 테이블의 REV 컬럼은 int 에서 long 으로 변환해 줘야 한다.
추가/수정/삭제를 할 때 마다 REV 가 하나씩 증가하는데, 20억이 넘어가면 int 사이즈는 오버 플로우 되어버릴 것이다.
RevisionEntity를 Custom 하게 만들어서 사용할 수 있습니다. (INT -> LONG)
@RevisionEntity와 @RevisionNumber, @RevisionTimestamp만 잘 설정해 주면 됩니다.
@RevisionEntity를 만들면 envers가 사용하는 모든 revision 기록은 이 entity를 사용하게 됩니다.
7. 그 외 부가적인 설명
ToOne 관계의 join column audit
If you want to audit a relation, where the target entity is not audited (that is the case for example with dictionary-like entities, which don't change and don't have to be audited), just annotate it with @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED). Then, when reading historic versions of your entity, the relation will always point to the “current” related entity.
- 관계맺은 엔티티 자체는 audit 하지 않고, join column 값의 변화면 검사하고자 한다면 관계 매핑에 @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) 어노테이션 사용
@JoinColumn을 사용한 @OneToMany 인 경우
@JoinColumn을 사용한 @OneToMany로 양방향 연관관계인 경우, one 쪽에 @JoinTable이나 @AuditMappedBy 설정을 해줘야 한다.
@Entity
@Audited
@AuditTable("order_audit")
class Order(
@OneToMany
@AuditMappedBy(mappedBy = "order")
private orderItems: List<OrderItems>,
)
@Version을 사용하는 경우
@Version 을 사용해서 optimistic locking 을 사용하는 경우, audit 테이블에 함께 저장하기 위해서는 do_not_audit_optimistic_locking_field 설정을 해줘야 한다.
org.hibernate.envers:
do_not_audit_optimistic_locking_field: false
# false 로 설정 해야 @Version 컬럼도 audit 테이블에 저장된다. default : true
삭제할 때도 스냅샷을 남기고 싶은 경우
현재 진행중인 프로젝트에서는 soft delete를 사용하고 있는데, Hibernate Envers는 hard delete 기준으로 만들어진 것 같다.
Envers는 데이터를 삭제하면 실제로 데이터를 지우는 형태로 REVTYPE=2(del) 와 모든 필드는 null 로 audit 테이블에 저장한다.
하지만, soft delete를 사용하면 일부 필드만 변경(deleted=true)을 하는 경우가 많고, 저장할 때의 값이 필요해질 때가 있다. 이런 경우, store_data_at_delete 설정을 해줘야 한다.
org:
hibernate:
envers:
store_data_at_delete: true # Delete 될 때, 현재 상태를 함께 저장한다. false 인 경우 null 로 저장됨. default: false
History 테이블을 조회할때 Repository
@Repository
public interface UserRepository extends JpaRepository<User,Long>, RevisionRepository<User, Long, Integer> {
}
참고
'Spring , JPA' 카테고리의 다른 글
Spring Cloud OpenFeign 사용법 (0) | 2023.11.12 |
---|---|
Spring Boot Logging (log4j, logback, log4j2) (0) | 2023.07.09 |
Spring Boot , Java Application Graceful Shutdown (0) | 2023.03.13 |
[spring] Spring Webflux 란? (0) | 2023.01.17 |
[spring] Spring Webflux CRUD (0) | 2023.01.16 |