Todo.class (부모 클래스)
@Entity
public class Todo extends DateEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TODO_ID")
private Long id;
@Column(nullable = false) // text
private String content; // 내용
@Column(name = "MEMBER_ID", nullable = false)
private String writer; // 작성자 -> Member
@Convert(converter = TodoCheckedConverter.class)
private boolean checked;
@OneToMany(mappedBy = "todo", fetch = FetchType.LAZY,
cascade = CascadeType.ALL, orphanRemoval=true )
@Builder.Default
private List<Comment> comments = new ArrayList<>();
public void addComments(Comment comment) {
this.comments.add(comment);
}
public void changeContent(String content) {
this.content = content;
}
public void changeChecked() {
this.checked = !checked;
}
}
Comment.class (자식 클래스)
@Entity
public class Comment extends DateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "COMMENT_ID")
private Long id;
@Column(name = "CONTENT", nullable = false)
private String content;
@Column(nullable = false)
private String writer; // 작성자 -> Member
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TODO_ID")
private Todo todo;
public void changeContent(String content) {
this.content = content;
}
}
삭제 관련 쿼리를 진행할 때, 보통 deleteById 를 사용한다.
하지만 deleteById 의 경우 delete SQL을 여러번 전송한다.
예제로 하나의 Todo 에 5개씩 Comment 를 달았다고 가정한다.
@BeforeEach
void BEFORE_CREATE_ENTITY() {
for(int j=0; j<3; j++) {
Todo entity = Todo.builder()
.content("todo"+j)
.writer("writer"+j)
.checked(false)
.build();
for(int i=0; i<=5; i++) {
entity.addComments(Comment.builder().content("안녕하세요."+i).writer("작성자"+i).todo(entity).build());
}
todoRepository.save(entity);
}
}
@DisplayName("todo Delete Test")
@Test
@Order(4)
void DELETE_TEST() {
todoRepository.deleteById(id);
assertThat(todoRepository.findById(id)).isEmpty();
}
결과 쿼리
Hibernate:
delete
from
comment
where
comment_id=?
Hibernate:
delete
from
comment
where
comment_id=?
Hibernate:
delete
from
comment
where
comment_id=?
Hibernate:
delete
from
comment
where
comment_id=?
Hibernate:
delete
from
comment
where
comment_id=?
Hibernate:
delete
from
comment
where
comment_id=?
Hibernate:
delete
from
todo
where
todo_id=?
Comment 를 5번 삭제하고, Todo 를 1번 삭제하는 쿼리가 전송된다.
만약 수억개의 답글이 달려있다면? 수억개의 delete SQL이 전송된다.
따라서 나는 연관관계 객체의 데이터를 같이 삭제해야 하는 위와 같은 상황인 경우
벌크성 삭제를 이용해서 JPQL 을 만들어 호출한다.
대신 로직이 추가될 것이다.
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Modifying
@Query("delete from Comment c where c.todo.id = :todoId")
public void deleteByTodoId(@Param("todoId") Long todoId);
}
Spring Data JPA 에서는 @Modifying 어노테이션을 설정해서 해당 JPQL 을 벌크성으로 변환시켜준다.
void DELETE_TEST() {
commentRepository.deleteByTodoId(id);
todoRepository.deleteById(id);
assertThat(todoRepository.findById(id)).isEmpty();
}
이제 추가적으로 delete 메서드를 호출해서 1번의 Delete SQL 을 해본다.
Hibernate:
delete
from
comment
where
todo_id=?
Hibernate:
delete
from
comment
where
comment_id=?
-> org.springframework.orm.ObjectOptimisticLockingFailureException:
Batch update returned unexpected row count from update [0];
actual row count: 0; expected: 1;
statement executed: delete from comment where comment_id=?;
에러가 발생한다. 그 이유는,,?
@OneToMany(mappedBy = "todo", fetch = FetchType.LAZY,
cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<Comment> comments = new ArrayList<>();
Cascade 와 orphanRemoval 속성으로 인한 동작방식을 알아야 할것 같다.
두 속성을 선언할 경우 부모 Entity 인 Todo 클래스에서 Comment 클래스의 영속성 생명주기를 관리하게 될 것이다.
Cascade.ALL 은 Persist 와 Remove 속성을 선언한 것과 같다.
Remove 는 부모 클래스를 삭제하면 자식 엔티티도 삭제하는 SQL 을 날린다.
orphanRemoval=true 또한 부모 클래스를 자식 엔티티도 삭제하려 한다.
두 설정을 같이 하는 이유는 Todo 의 Comment 컬렉션 중 하나를 삭제 할 때 Delete SQL 을 날리기 위함이다.
원했던 동작방식은 다음과 같다.
- 여러 개일 수 있는 Comment 를 한번의 SQL 로 삭제하기 위해서 벌크성의 JPQL 을 만들어서 호출했다.
- 이후 부모 Entity 인 Todo 를 삭제한다.
하지만, 나타난 SQL 은 Todo 의 deleteById 로 인해서 자식 Entity를 삭제하려는 SQL 을 날렸다.
Hibernate:
delete
from
comment
where
comment_id=?
해결을 하는 방법은 다음과 같다.
Cascade 속성을 Persist 로 변경하고, orphanRemoval 을 제거한다.
@OneToMany(mappedBy = "todo", fetch = FetchType.LAZY,
cascade = CascadeType.PERSIST)
@Builder.Default
private List<Comment> comments = new ArrayList<>();
// 호출결과
Hibernate:
delete
from
comment
where
todo_id=?
Hibernate:
delete
from
todo
where
todo_id=?
해결은 할 수 있지만 이렇게 할 경우 부모클래스에서 컬렉션 중 하나의 값을 지우더라도 Delete SQL 을 날리지 않는다.
여기서 트레이드 오프가 필요하다고 보인다.
부모 Entity 인 Todo 에 답글이 많이 달릴 것이라고 가정하고 설계한다면
위 해결방식이 좀 더 성능적으로 가까울 것이라고 본다.
물론 이렇게 한다면 하나의 ROW 를 삭제하는 비즈니스 로직이 추가될 거라고 생각한다.
반면에, 답글이 많이 달리지 않을 것이라고 한다면, 그냥 deleteById 를 하나만 호출함으로써 자식 Entity 인 답글의 DB를 하나씩 삭제하게끔 비즈니스 로직을 추가하지 않고 자동으로 SQL 을 전달하는 것도 맞을것이라고 본다.
'Back-End > JPA' 카테고리의 다른 글
querydsl 단 건 조회시 firstResult/maxResults specified with collection fetch; applying in memory! (0) | 2022.09.02 |
---|---|
JPA 연관관계 양방향 매핑 Entity 조회시 문제&해결 (0) | 2022.04.28 |
JPA) 벌크 연산 (0) | 2022.03.30 |
JPA) Join Fetch, @EntityGraph 차이점 (0) | 2022.03.24 |
JPA) Spring OSIV (0) | 2022.03.14 |