EntityListener는 해당 엔티티 라이프 사이클 중 특점 시점에 원하는 로직을 처리하도록 한다 .
총 7가지가 있으며 각각 엔티티의 라이프 사이클에 맞게 동작한다
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Data
@Builder
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@NonNull
private String name;
@NonNull
private String email;
private String nickName;
@Transient
private String testData;
@Enumerated(value = EnumType.STRING)
private Gender gender;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
/*
Entity Listener 종류
이벤트가 발생하기 직전 혹은 직후에 해당 이벤트 발생을 감지하고 정의한 로직을 처리한다.
@PrePersist
@PreUpdate
@PreRemove
@PostPersist
@PostUpdate
@PostRemove
@PostLoad
*/
@PrePersist
public void prePersist(){
System.out.println(">>>prepersist");
}
@PostPersist
public void postPersist(){
System.out.println(">>>postPersist");
}
@PreUpdate
public void preUpdate(){
System.out.println(">>>preUpdate");
}
@PostUpdate
private void postUpdate(){
System.out.println(">>>postUpdate");
}
@PreRemove
private void preRemove() {
System.out.println(">>>preRemove");
}
@PostRemove
private void postRemove(){
System.out.println(">>>postRemove");
}
@PostLoad
private void postLoad(){
System.out.println(">>>postLoad");
}
}
확인을 위해각 리스너 별로 해당 리스너의 이름이 찍히게 하는 메서드를 만들어 봤다 .
테스트를 작성해 확인해 보자
@Test
void entityListenerTest (){
User user = new User();
user.setName("ugo");
user.setEmail("staasd04@gmail.com");
//insert
userRepository.save(user);
//update
User user2 = userRepository.findById(1L).orElseThrow(RuntimeException::new);
user2.setName("ugo222");
userRepository.save(user2);
//delete
userRepository.deleteById(1L);
}
테스트를 돌려보면 각각의 쿼리 이전 이후로 해당 메서드의 로그가 찍힌다 .
postLoad의 경우는 엔티티가 select 되어 로딩될때 찍힌다.
@PrePersist @PreUpdate 두가지를 많이 쓰는데 이유는 보통 해당 엔티티가 언제 생성되었는지 언제 수정되었는지에 대한
정보를 갖기 때문에 엔티티에 정보를 넣을때 마다 그 값을 넣는 것 보다는 어노테이션을 사용해 insert 시점이나 update 시점에 들어 가도
록 할 수 있기 때문이다 .
@PrePersist
public void prePersist(){
this.setCreatedAt(LocalDateTime.now());
this.setUpdatedAt(LocalDateTime.now());
}
@PreUpdate
public void preUpdate(){
this.setUpdatedAt(LocalDateTime.now());
}
간단하다 해당 엔티티에 원하는 어노테이션을 붙혀 메서드를 만들면된다.
생성시점에 createdAt , UpdatedAt 이 set 될 것이며 update 시점에 UpdatedAt이 업데이트 될 것이다 .
테스트 해보자
@Test
void preUpdateTest(){
//유저 생성
User user = new User();
user.setEmail("ugo@gmail.com");
user.setName("ugo");
//insert
userRepository.save(user);
//insert된 User의 updatedAt 과 update된 후의 updatedAt 을 비교 해보려 한다.
LocalDateTime createdAt = userRepository.findByEmail("ugo@gmail.com").getUpdatedAt();
System.out.println("updateAtWhenCreated = " + createdAt);
//update
User byEmail = userRepository.findByEmail("ugo@gmail.com");
byEmail.setName("ugogogo");
userRepository.save(byEmail);
//update된 updatedAt
LocalDateTime updatedAt = userRepository.findByEmail("ugo@gmail.com").getUpdatedAt();
System.out.println("updatedAt = " + updatedAt);
Assertions.assertNotEquals(updatedAt,createdAt);
}
insert 시점의 updateAt과 update 시점의 updateAt이 달라야한다.
테스트가 잘 통과 됬고 결과는 아래 그림과 같다 .
위와 같은 경우 다른 엔티티에도 똑같이 사용되는 것이기 때문에 리스너를 만들어서 해당 기능을 원하는 엔티티에서 사용할 수 있게 만들어 주는 것이 좋다 .
리스너를 만들어 사용하는 엔티티에서 @EntityListener(리스너클래스.class) 을 지정하면 해당 기능이 사용가능하다 .
리스너를 만들어보자
package com.ugo.jpatest.domain.entitylistener;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;
public class TimeStampEntityListener {
//리스너는 여러 곳에서 사용되고 파라미터로 들어오는 것이 어떤타입인지 모르기 떄문에 Object로 받아야 한다.
//현재 리스너를 사용하는 것은 Book 과 user 이다 .
//여러 곳에서 사용하고 있기 떄문에 인터페이스를 만들어 사용하는 곳에서 인터페이스를 구현하게 하면 코드를 줄일 수 있다.
//만약 인터페이스를 사용하지 않는다면 if ( instanceof )로 타입검사를 하는 것이 리스너를 사용하는 클래스 수만큼 늘어날 것이다.
@PrePersist
public void prePersist(Object o){
if(o instanceof Auditable){
((Auditable) o).setCreatedAt(LocalDateTime.now());
((Auditable) o).setUpdatedAt(LocalDateTime.now());
}
}
@PreUpdate
public void preUpdate(Object o ){
if(o instanceof Auditable){
((Auditable) o).setUpdatedAt(LocalDateTime.now());
}
}
}
TimeStampEntityListener 클래스를 만들었다 해당 클래스에 메서드를 정의해놓으면 사용하는 엔티티에서는 해당 기능을 직접
구현하지 않아도 기능이 동작한다 .
리스너에 @Prepersist @PreUpdate가 달린 메서드를 만들었다 .
메서드는 여러 곳에서 사용될꺼고 리스너는 누가 어디서 사용하는지 알 수 없다
이를 위해서 파라미터로 Object 타입으로 받게 강제되어있다.
파라미터를 Object 로 받아서 메서드 안에서 instanceof로 타입 검사 후에 캐스팅하여 사용하면 된다 .
여기서 문제는 해당 리스너를 사용하는 클래스 갯수만큼 타입 검사를 해야해 반복적인 코드를 작성해야 한다.
이럴떄 인터페이스를 사용하면 좋다 .
Auditable 이라는 인터페이스를 만들었다
package com.ugo.jpatest.domain.entitylistener;
import java.time.LocalDateTime;
public interface Auditable {
LocalDateTime getCreatedAt();
LocalDateTime getUpdatedAt();
void setCreatedAt(LocalDateTime localDateTime);
void setUpdatedAt(LocalDateTime localDateTime);
}
Auditable 인터페이스를 구현하는 객체는 다형성을 통해 Auditable 타입으로 사용할 수 있으니
auditable 인터페이스에 리스너에서 사용될 메서드들만 정의해 놓으면 타입 검사는 auditable 한가지로만 하면된다 .
리스너에서는 createdAt과 updatedAt 두가지를 set해주는 기능이 필요하다.
auditable이 갖어야할 메서드는 setCreatedAt(LocalDateTime ldt), setUpdatedAt(LocalDateTime ldt) 두 가지면 될 것 같다 .
리스너로 돌아와 보자
@PrePersist를 단 메서드를 만들고 파라미터를 오브젝트로 받는다 , auditable 인터페이스로 타입 검사를 한다.
Auditable 타입이면 if문 안으로 들어올 것이다 . if 문안에서 Auditable로 캐스팅하고 createdAt과 updatedAt을 set 해준다 .
user와 book에 있던 @Prepersist @PreUpdate 메서드를 지우고 동작을 확인해 보자
잘된다 .
한 군데에서 사용될 리스너기능 같은경우에는 해당엔티티에 직접 정의하고
여러군데에서 사용될 리스너기능은 리스너를 직접 구현해 사용하는 것이 좋을 것 같다 .
'JPA > Spring data JPA' 카테고리의 다른 글
SPRING DATA JPA 공부 7 - AuditingEntityListener 사용 (0) | 2021.05.29 |
---|---|
SPRING DATA JPA 공부 4 - Query Method 활용 2 ( 정렬과 페이징) (1) | 2021.05.28 |
SPRING DATA JPA 공부 3 - QueryMethod 활용 (0) | 2021.05.27 |
SPRING DATA JPA 공부 2- JPA 메서드 살펴보기 (0) | 2021.05.27 |
SPRING DATA JPA 공부 1 - JPA 란? (0) | 2021.05.27 |