문제상황 : 댓글에 좋아요가 눌린 뒤 댓글이 삭제되면 좋아요도 DB에서 삭제돼야했지만 삭제가 되지않았다.
외래키를 참조하고있기 때문에 삭제를 할 수 없다는 에러다.
먼저 Entity는 아래와 같다.
@Entity
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long commentId; // 댓글 식별자
@Column(nullable = false)
private String content; // 댓글 내용
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
@Nullable
private Comment parent;
//댓글과 대댓글의 그룹 ID
private Long groupId;
@ColumnDefault("false")
private boolean isDeleted;
@OneToMany(mappedBy = "comment", cascade = CascadeType.ALL)
private List<LikeComment> likeComments = new ArrayList<>();
}
댓글 엔티티는 맨 마지막 LikeComment를 List로 가지고 있으며 OneToMany 관계이다.
LikeComment Entity는 아래와 같다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LikeComment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "commentId")
private Comment comment;
}
LikeComment는 Comment의 좋아요를 어떤 회원이 눌렀는지를 저장하는 테이블이다. LikeComment 입장에서 ManyToOne관계다.
아래는 Service 클래스에서 댓글을 삭제하는 로직이다.
@Transactional
public Long deleteComment(Long requestUserId, Long boardId, Long commentId) throws BaseException {
// 게시판 검증
utilService.findByBoardIdWithValidation(boardId);
// 댓글 검증
Comment deleteRequestComment = utilService.findByCommentIdWithValidation(commentId);
// 댓글 원작자 회원 ID
Long originUserId = deleteRequestComment.getUser().getId();
// 댓글 원작자 검증
isOriginalWriter(requestUserId, originUserId);
// 부모 댓글인 경우
if (deleteRequestComment.getParent() == null) {
deleteParentComment(deleteRequestComment, commentId);
} else {
Comment parentComment = deleteRequestComment.getParent();
deleteChildComment(parentComment, commentId);
}
return commentId;
}
아래는 댓글을 삭제하는 리포지토리다.
@Modifying
@Query("delete from Comment c where c.commentId = :commentId")
void deleteCommentByCommentId(@Param("commentId") Long commentId);
분명 Cascade 옵션을 ALL로 주어 모든 생명주기를 부모엔티티에 따르도록 설정했다.
그렇다면 댓글이 삭제되면 좋아요도 삭제돼야한다고 생각했다.
첫 번째 문제점 [ 댓글을 삭제해도 댓글 좋아요 엔티티가 제거되지않는다 ]
JPA cascading delete fails with custom delete method
I ran into an error with custom delete method in spring data jpa. Basically there's a bag which contains items, and when deleting the bag, all the items in it should be deleted. Here're the entitie...
stackoverflow.com
나와 같은 질문을 하는 질문이 올라와있다.
문제 원인을 간단히 요약하자면 리포지토리에서 @Query 어노테이션이 붙은 메서드에 있었다.
@Modifying
@Query("delete from Comment c where c.commentId = :commentId")
void deleteCommentByCommentId(@Param("commentId") Long commentId);
@Query는 JPQL을 작성해 영속성컨텍스트를 거치지않고 DB에 바로 쿼리가 날아간다.
이게 문제인 이유는 Cascade 옵션은 Hibernate의 로직이다. 하지만 @Query를 작성해 DB에 쿼리를 바로 날리면 Hibernate의 로직을 타지않는다는 것이다.
스택오버플로우의 답변중에서 @Query를 활용하면서 자식까지 함께 지우는 방법이 나와있긴하다.
DB에 직접 cascade 설정을 걸어주면 된다.
하지만 DB에 cascade 옵션을 직접 걸어주고싶지않았다.
결국 직접 DB에 질의하는 @Query를 사용하지않고 기본으로 제공하는 deleteById를 사용했다.
그렇다면 왜 굳이 deleteById를 냅두고 @Query를 사용했는지
사실 나의 오해에 기반한 실수였다.
@Query 어노테이션으로 직접 작성한 JPQL은 AUTO Flush 할까?
Auto Flush 조건 지금까지 알고있었던 자동 flush 조건은 셋 중 하나였다. em.flush() 호출 트랜잭션 Commit 시점 JPQL 호출 직전 정말 JPQL 호출 직전 flush 할까? 눈으로 확인해보자. JQPL 호출 직전 flush 한다
curiosity-s.tistory.com
무슨 오해였는지는 위 포스트에서 자세하게 다루었다.
간단히 요약하자면 @Query 메서드가 수행하는 호출 직전 flush가 필요했다.
하지만 @Query만 호출 직전에 flush가 이루어지는게 아니였다.
하이버네이트는 JPQL를 질의하기전에 영속성컨텍스트에서 해당 엔티티의 중복된 작업이 있다면 먼저 flush한다.
@Query는 DB에 직접 질의한다는 점 말고는 JPARepository에서 기본제공하는 메서드, Query Method 네이밍기반으로 작성한 메서드 모두 위에서 말한 조건하에 flush하게된다.
'Spring' 카테고리의 다른 글
[Spring Security JWT 로그인 (2)] JWT가 탈취된다면 어떻게 대응해야 할까 (0) | 2024.03.10 |
---|---|
Redis Session Clustering [세션 고정] (0) | 2023.12.17 |
S3를 이용한 사용자의 고아 이미지 처리 (1) | 2023.12.02 |
코드의 중복과 Null 처리 (0) | 2023.09.25 |
@Query 어노테이션으로 직접 작성한 JPQL은 AUTO Flush 할까? (0) | 2023.07.20 |