spring-data-jpa에서 AbstractAuditable을 이용한 audit 남기기
- IT/PROGRAMMING
- 2024. 10. 26. 23:24
안녕하세요, 하마연구소 입니다.
spring-data-jpa에서 entity의 CUD 이력을 남기기 위한 audit 기능을 제공해줍니다.
몇몇 설정한하면 아주 편리하죠.
https://docs.spring.io/spring-data/jpa/reference/auditing.html
Audit 데이터는 보통 생성자(createdBy), 생성일시(createdDate), 수정자(lastModifiedBy), 수정일시(lastModifiedDate)를 기록하며, 아래와 같이 추상클래스로 정의하여 사용하기도 합니다.
@MappedSuperclass
@EntityListeners(value = [AuditingEntityListener::class])
@Audited
abstract class BaseEntity(
@CreatedBy
@Column(nullable = false, updatable = false)
var createdBy: String? = null,
@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = false, updatable = false)
var createdDate: Instant? = null,
@LastModifiedBy
@Column(nullable = false, updatable = true)
var lastModifiedBy: String? = null,
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = false, updatable = true)
var lastModifiedDate: Instant? = null,
)
스프링에서는 이러한 audit 필드 정의를 해둔 클래스가 존재합니다.
Auditable 인터페이스로 정의하였으며, 구현체로 AbstractAuditable 추상클래스가 있습니다.
package org.springframework.data.domain;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;
/**
* Interface for auditable entities. Allows storing and retrieving creation and modification information. The changing
* instance (typically some user) is to be defined by a generics definition.
*
* @param <U> the auditing type. Typically some kind of user.
* @param <ID> the type of the audited type's identifier
* @author Oliver Gierke
*/
public interface Auditable<U, ID, T extends TemporalAccessor> extends Persistable<ID> {
/**
* Returns the user who created this entity.
*
* @return the createdBy
*/
Optional<U> getCreatedBy();
/**
* Sets the user who created this entity.
*
* @param createdBy the creating entity to set
*/
void setCreatedBy(U createdBy);
/**
* Returns the creation date of the entity.
*
* @return the createdDate
*/
Optional<T> getCreatedDate();
/**
* Sets the creation date of the entity.
*
* @param creationDate the creation date to set
*/
void setCreatedDate(T creationDate);
/**
* Returns the user who modified the entity lastly.
*
* @return the lastModifiedBy
*/
Optional<U> getLastModifiedBy();
/**
* Sets the user who modified the entity lastly.
*
* @param lastModifiedBy the last modifying entity to set
*/
void setLastModifiedBy(U lastModifiedBy);
/**
* Returns the date of the last modification.
*
* @return the lastModifiedDate
*/
Optional<T> getLastModifiedDate();
/**
* Sets the date of the last modification.
*
* @param lastModifiedDate the date of the last modification to set
*/
void setLastModifiedDate(T lastModifiedDate);
}
package org.springframework.data.jpa.domain;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Optional;
import org.springframework.data.domain.Auditable;
import org.springframework.lang.Nullable;
@MappedSuperclass
public abstract class AbstractAuditable<U, PK extends Serializable> extends AbstractPersistable<PK> implements Auditable<U, PK, LocalDateTime> {
@ManyToOne
@Nullable
private U createdBy;
@Temporal(TemporalType.TIMESTAMP)
@Nullable
private Date createdDate;
@ManyToOne
@Nullable
private U lastModifiedBy;
@Temporal(TemporalType.TIMESTAMP)
@Nullable
private Date lastModifiedDate;
public AbstractAuditable() {
}
public Optional<U> getCreatedBy() {
return Optional.ofNullable(this.createdBy);
}
public void setCreatedBy(U createdBy) {
this.createdBy = createdBy;
}
public Optional<LocalDateTime> getCreatedDate() {
return this.createdDate == null ? Optional.empty() : Optional.of(LocalDateTime.ofInstant(this.createdDate.toInstant(), ZoneId.systemDefault()));
}
public void setCreatedDate(LocalDateTime createdDate) {
this.createdDate = Date.from(createdDate.atZone(ZoneId.systemDefault()).toInstant());
}
public Optional<U> getLastModifiedBy() {
return Optional.ofNullable(this.lastModifiedBy);
}
public void setLastModifiedBy(U lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Optional<LocalDateTime> getLastModifiedDate() {
return this.lastModifiedDate == null ? Optional.empty() : Optional.of(LocalDateTime.ofInstant(this.lastModifiedDate.toInstant(), ZoneId.systemDefault()));
}
public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
this.lastModifiedDate = Date.from(lastModifiedDate.atZone(ZoneId.systemDefault()).toInstant());
}
}
이 AbstractAuditable 추상클래스를 base-entity로 삼아서 사용할 Entity를 정의하고 사용하면 됩니다.
심지어 이 추상클래스에는 @Id 필드도 정의되어 있어 Entity 객체에서 별도로 ID 필드를 정의하지 않아도 됩니다.
아래는 AbstractAuditable을 이용한 MyTableEntity 샘플입니다.
import jakarta.persistence.Entity
import jakarta.persistence.EntityListeners
import jakarta.persistence.Table
import org.hibernate.envers.Audited
import org.springframework.data.jpa.domain.AbstractAuditable
import org.springframework.data.jpa.domain.support.AuditingEntityListener
@Entity
@Table(name = "myTable")
@EntityListeners(value = [AuditingEntityListener::class])
@Audited
data class MyTableEntity(
var name: String? = null,
var description: String? = null,
) : AbstractAuditable<MyUser, Long>()
AbstractAuditable에서 User 타입을 나타내는 U generic 타입은 일반적으로 String을 많이 사용할 것입니다.
즉, DB 컬럼 값으로 String 타입을 사용하기에 Entity도 String으로 정의합니다.
따라서 다음과 같이 U의 타입을 String으로 사용하겠죠.
@Entity
data class MyTableEntity(
.
.
.
) : AbstractAuditable<String, Long>()
그러나...
이러면 컴파일 오류가 발생합니다.
이유는 String 객체가 @Entity가 아니라고 합니다.
Caused by: org.hibernate.AnnotationException: Association 'com.kakao.account.mulang.entity.ApplicationEntity.createdBy' targets the type 'java.lang.String' which is not an '@Entity' type
at org.hibernate.boot.model.internal.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:110) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processEndOfQueue(InFlightMetadataCollectorImpl.java:1906) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processFkSecondPassesInOrder(InFlightMetadataCollectorImpl.java:1855) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1764) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:334) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
현재는 AbstractAuditable의 U 타입은 반드시 @Entity여야 하며, 따라서 String 타입은 사용하지 못합니다.
아쉽네요.
만약 User 데이터가 @Entity로 정의되어 있다면 AbstractAuditable 사용하는 것도 고려할만하겠네요.
감사합니다.
'IT > PROGRAMMING' 카테고리의 다른 글
워치 화면, 제가 원하는 것을 만들고 있습니다. (0) | 2024.05.30 |
---|---|
Kotlin 2.0.0, 공식 릴리즈 되었군요. (0) | 2024.05.21 |
Java로 HTML 처리는 jsoup이 짱이네요. (0) | 2024.05.21 |
Kotlin의 모든 클래스에서 logger 객체를 편하게 얻을 수 있는 방법 (1) | 2021.08.18 |
[Spring] spring-boot 2.1(SpringFramework 5.1)에서 없어진 기능, JSONP 간단하게 구현하기 (0) | 2019.06.09 |