Spring , JPA

Spring Data Envers 사용방법 - 간편하게 히스토리 테이블 관리하기

seulseul 2023. 11. 13. 22:52

개요

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);
https://www.geeklearners.com/2019/03/hibernate-envers-history-data-and.html

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> {

}

 
 
 
 
 

참고