[JPA] Entity Listener와 JPA CRUD

keymu·2024년 12월 27일
1

JPA Entity

1. 둘의 차이가 뭘까?

uniqueConstraints

email에만 unique 설정

정답: uniqueConstraints는 복합키로도 unique 설정 가능


2. save는 언제 INSERT/UPDATE일까?

@Test
void insertAndUpdateTest() {
    System.out.println("\n-- TEST#insertAndUpdateTest() ---------------------------------------------");
    User user = new User();
    user.setName("kname");
    user.setEmail("kname@mail.com");

    userRepository.save(user);  // INSERT

    user2=userRepository.findById(1L).orElseThrow(RuntimeException::new);
    user2.setName("pname");

    userRepository.save(user2); // UPDATE

    System.out.println("\n------------------------------------------------------------\n");
}
  • id를 확인하고 있으면 update, 없으면 insert

3. @Transient

  • 영속성 처리에서 배제되고, 굳이 db에 반영하고 싶지 않음
  • 단기적으로 잠시 머무르는 변수

4. ENUM

  • JPA는 기본적으로 enum을 정수 타입으로 저장(0, 1 등)
  • 문자 타입으로 저장할 수 있도록 설정해주어야 함


Listener 사용법

Entity Listener

  • 특정 이벤트에서 Entity 관련으로 동작

  • auditing: PrePersist, PreUpdate 많이 쓰임

1. Entity 객체 내부에서 선언

@PrePersist
    public void prePersist() {
        System.out.println(">>> prePersist");
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        System.out.println(">>> preUpdate");
        this.updatedAt = LocalDateTime.now();
    }

    @PreRemove
    public void preRemove() {
        System.out.println(">>> preRemove");
    }

    @PostPersist
    public void postPersist() {
        System.out.println(">>> postPersist");
    }

    @PostUpdate
    public void postUpdate() {
        System.out.println(">>> postUpdate");
    }

    @PostRemove
    public void postRemove() {
        System.out.println(">>> postRemove");
    }

    @PostLoad
    public void postLoad() {
        System.out.println(">>> postLoad");
    }
  • 객체 내부에 선언 후 사용
  • 현재시간 입력을 위해 매번 반복되는 코드 / 데이터 정확도 문제
  • 여러 Entity에 동일하게 사용되는 Listener를 담은 클래스 사용하자

2. 별도의 Listener Class

public interface Auditable {
    LocalDateTime getCreatedAt();
    LocalDateTime getUpdatedAt();

    void setCreatedAt(LocalDateTime createdAt);
    void setUpdatedAt(LocalDateTime updatedAt);
}
@EntityListeners(value = {MyEntityListener.class})
public class User implements Auditable {
  • Listener class 사용 시, 공통적 부분에 대한 listener를 하나만 구현하여 @EntityListeners를 사용해 참조하여 사용가능하다. 반복적 코딩을 줄일 수 있다.

+ Log Data (히스토리 데이터)에 "곧 수정될 내용"을 history 담기

  • event listener에서 특정값을 추가하는 경우 말고, 특정 데이터가 수정되면 해당값의 복사본을 다른 테이블에 저장해두는 event일 경우
  • User Entity에 User data라는 중요 데이터가 있으므로, 수정된 내역의 히스토리를 필요해 보임.
import com.lec.spring.domain.UserHistory;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserHistoryRepository extends 
	JpaRepository<UserHistory, Long> {
@Component
public class UserEntityListener {

    @Autowired
    private UserHistoryRepository userHistoryRepository;

    @PreUpdate  // User 가 UPDATE 수행하기 전
    @PrePersist     // User 가 INSERT 수행하기 전
    public void addHistory(Object o) {
        System.out.println(">>UserEntityListener addHistory() 호출");

        User user = (User) o;
        // UserHistory 에 UPDATE 될 User 정보 담아서 저장 (INSERT)
        UserHistory userHistory = new UserHistory();
        userHistory.setUserId(user.getId());
        userHistory.setName(user.getName());
        userHistory.setEmail(user.getEmail());
        
        userHistoryRepository.save(userHistory);    // INSERT
    }
@EntityListeners(value = {MyEntityListener.class, UserEntityListener.class})
  • but,Entity Listener는 Spring Bean 주입받지 못해 NPE 발생
// Listener 안에서 스프링 빈 객체 수동으로 주입받기
        UserHistoryRepository userHistoryRepository = BeanUtils.getBean(
        UserHistoryRepository.class);
  • UserHistory에도 @EntityListeners 설정 : INSERT, UPDATE될 때, createdAt, updatedAt이 이제 자동으로 세팅된다. 코드의 반복이 줄어들었음.
@EntityListeners(value = MyEntityListener.class)
  • 생성일, 수정일은 워낙 많은 Entity에서도 사용하여, Spring에서도 기본 Listener를 제공하고 있다.

3. Spring에서 제공하는 AuditingEventListener 사용

@EnableJpaAuditing

@EntityListeners(value = AuditingEntityListener.class)
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
  • 이렇게 되면, createdAt, updatedAt를 엄청 반복하니, 리팩토링해서 AuditingEntityListener 사용해보자.

BaseEntity

@Data
@MappedSuperclass   // (JPA)가 이 클래스의 속성을, 상속받는 Entity 에 포함시켜줌
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity{

    @CreatedDate
    private LocalDateTime createdAt;
    @LastModifiedDate
    private LocalDateTime updatedAt;

}
@EntityListeners(value = {UserEntityListener.class})
public class User extends BaseEntity implements Auditable {

Lombok의 toString 문제 해결

// 부모쪽도 toString으로 호출
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
  • 매 Entity마다 Auditable을 implement 할 필요없이, BaseEntity만 Auditable을 implement하게 하면 된다.
public class User extends BaseEntity {
@Data
@MappedSuperclass   // (JPA)가 이 클래스의 속성을, 상속받는 Entity 에 포함시켜줌
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity implements Auditable {

    @CreatedDate
    private LocalDateTime createdAt;
    @LastModifiedDate
    private LocalDateTime updatedAt;

}

JPA CRUD

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}
@Service
public class BoardServiceImpl implements BoardService {

    private final PostRepository postRepository;

    public BoardServiceImpl(PostRepository postRepository) {
        this.postRepository = postRepository;
    }

    @Override
    public int write(Post post) {
        postRepository.save(post);
        return 1;
    }

    @Override
    public Post detail(Long id) {
        Optional<Post> postOptional = postRepository.findById(id);
        if (postOptional.isPresent()) {
            Post post = postOptional.get();

            if (post.getViewCnt() == null) {
                post.setViewCnt(1L);
            } else {
                post.setViewCnt(post.getViewCnt() + 1);
            }

            postRepository.save(post);
            return post;
        }
        return null;
    }


    @Override
    public List<Post> list() {
        return postRepository.findAll(Sort.by(Sort.Order.desc("id")));
    }

    @Override
    public Post selectById(Long id) {
        return postRepository.findById(id).orElse(null);
    }

    @Override
    public int update(Post post) {
        // 주의! 위 post 매개변수에는 viewcnt 값이 없기 때문에
        // 위 post 값으로 save 하면 viewcnt 값은 0 으로 초기화 된다.

        Optional<Post> existingPost = postRepository.findById(post.getId());
        if (existingPost.isPresent()) {
            Post updatedPost = existingPost.get();
            updatedPost.setSubject(post.getSubject());
            updatedPost.setContent(post.getContent());
            postRepository.save(updatedPost);
            return 1;
        }
        return 0;
    }

    @Override
    public int deleteById(Long id) {
        if (postRepository.existsById(id)) {
            postRepository.deleteById(id);
            return 1;
        }
        return 0;
    }
}

이런 식으로 사용된다.
refactoring 하면서 허무한 마음이 들었다. 내가 쓰던 sql query문들은 어디로..

물론 relation에 대해 배우면 좀 더 복잡해지겠지만, 일단 이걸로 돌아간다니!
ORM을 쓰는 이유가 너무나 뼈저리게 느껴졌다.


JPA Entity Relation

  • Entity는 양방향 참조하는 경우가 많다.

1:1

  • 지속적으로 서비스 무중단 제공해야 하고, 속성을 추가해야하는 방식의 컬럼이라면, 1:1 연관관계를 맺는 테이블을 추가하는 것도 방법.

3편에서 이어서

profile
Junior Backend Developer

0개의 댓글