이번 포스팅에서는 복잡한 연관관계에서의 cascade 옵션의 이해와 쿼리에 대해 설명하고자 한다.
부모 엔티티가 영속화될 때 자식 엔티티도 같이 영속화되고, 부모 엔티티가 삭제될 때 자식 엔티티도 삭제되는 등 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 전이되는 것을 의미합니다.
이번 포스팅은 JPA 에 대해 어느정도 알고 있다 생각하고 작성하기 때문에 영속성 전이 cascade 가 무엇인지 모른다면 해당 블로그를 참고하자!
https://willseungh0.tistory.com/67
이번 포스팅에서 사용되는 엔티티 및 연관관계도 이전 포스팅 그대로이다.
package couch.camping.domain.post.entity;
import couch.camping.domain.base.BaseEntity;
import couch.camping.domain.comment.entity.Comment;
import couch.camping.domain.member.entity.Member;
import couch.camping.domain.postimage.entity.PostImage;
import couch.camping.domain.postlike.entity.PostLike;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Post extends BaseEntity {
@Id @GeneratedValue
@Column(name = "post_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@Builder.Default
@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>();
@Builder.Default
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostImage> postImageList = new ArrayList<>();
@Builder.Default
@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<PostLike> postLikeList = new ArrayList<>();
@Lob
private String content;
private String postType;
private int likeCnt;
private int commentCnt;
public void editPost(String content, String hashTag) {
this.content = content;
this.postType = hashTag;
}
public void increaseLikeCnt() {
this.likeCnt = likeCnt+1;
}
public void decreaseLikeCnt() {
this.likeCnt = likeCnt - 1;
}
}
commentList, postImageList, postLikeList 와 같은 일대다 리스트를 보면 cascade 옵션이 달려있는 것을 확인할 수 있다.
package couch.camping.domain.postlike.entity;
import couch.camping.domain.member.entity.Member;
import couch.camping.domain.post.entity.Post;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PostLike {
@Id @GeneratedValue
@Column(name = "post_like_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
package couch.camping.domain.postimage.entity;
import couch.camping.domain.base.BaseEntity;
import couch.camping.domain.member.entity.Member;
import couch.camping.domain.post.entity.Post;
import lombok.*;
import javax.persistence.*;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class PostImage extends BaseEntity {
@Id @GeneratedValue
@Column(name = "post_image_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
private String imgUrl;
public PostImage(Member member, Post post, String imgUrl) {
this.member = member;
this.post = post;
this.imgUrl = imgUrl;
}
}
package couch.camping.domain.comment.entity;
import couch.camping.domain.base.BaseEntity;
import couch.camping.domain.commentlike.entity.CommentLike;
import couch.camping.domain.member.entity.Member;
import couch.camping.domain.post.entity.Post;
import lombok.*;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Comment extends BaseEntity {
@Id @GeneratedValue
@Column(name = "comment_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
private Post post;
@Builder.Default
@OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE, orphanRemoval = true)
List<CommentLike> commentLikeList = new ArrayList<>();
@Lob
private String content;
private int likeCnt;
public void editComment(String content){
this.content = content;
}
public void increaseLikeCnt() {
this.likeCnt = likeCnt+1;
}
public void decreaseLikeCnt() {
this.likeCnt = likeCnt - 1;
}
}
comment 엔티티 필드에 commentLikeList @OneToMany 리스트를 보면 cascade 옵션이 달려있는 것을 확인할 수 있다.
package couch.camping.domain.commentlike.entity;
import couch.camping.domain.comment.entity.Comment;
import couch.camping.domain.member.entity.Member;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class CommentLike {
@Id
@GeneratedValue
@Column(name = "comment_like_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "comment_id")
private Comment comment;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
그럼 post 엔티티를 영속화 할 때 따로 postImage 들도 하나하나 em.save() 를 해야할까?
아니다 save 는 한번만 호출해줘도 괜찮다.
Post post = Post.builder()
.title(postWriteRequestDto.getTitle())
.content(postWriteRequestDto.getContent())
.postType(postWriteRequestDto.getPostType())
.lastModifiedDate(LocalDateTime.now())
.member(member)
.build();
// Post 엔티티 postImageList 필듸에 add() 하는 로직
for (String imgUrl : postWriteRequestDto.getImgUrlList()) {
PostImage postImage = new PostImage(member, post, imgUrl);
post.getPostImageList().add(postImage);
}
Post savePost = postRepository.save(post);
위의 코드는 request DTO 를 받아 post 를 em.save() 하는 로직이다. 하지만 postImage 엔티티를 em.save() 해주는 코드를 찾을 수 없다. 그 이유는
cascade 옵션이 CascadeType.ALL 이기 때문에 post 엔티티의 postImageList 에 add() 하여 마지막에 post 엔티티를 save 하면 List 내부에 있는 postImage 들도 같이 저장된다.(위 코드 주석 참조)
게시글(post)에 3개의 사진(postImage)을 작성 후 게시글을 저장하면 아래와 같은 쿼리가 발생한다.
그 이유는 위에서 설명했듯이 1 개의 Post를 저장 시 3개의 postImage 를 같이 저장 하기 때문이다.
insert
into
post
(comment_cnt, content, created_by, created_date, last_modified_date, like_cnt, member_id, post_type, title, post_id)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
>>>>>>>>> 게시글 저장 쿼리
insert
into
post_image
(created_date, last_modified_date, created_by, last_modified_by, img_url, member_id, post_id, post_image_id)
values
(?, ?, ?, ?, ?, ?, ?, ?)
>>>>>>>> 첫번째 게시글 이미지 저장 쿼리
insert
into
post_image
(created_date, last_modified_date, created_by, last_modified_by, img_url, member_id, post_id, post_image_id)
values
(?, ?, ?, ?, ?, ?, ?, ?)
>>>>>>>>> 두번째 게시글 이미지 저장 쿼리
insert
into
post_image
(created_date, last_modified_date, created_by, last_modified_by, img_url, member_id, post_id, post_image_id)
values
(?, ?, ?, ?, ?, ?, ?, ?)
>>>>>>>>> 세번째 게시글 이미지 저장 쿼리
이번 포스팅의 핵심이다.
그럼 post 엔티티를 삭제할 경우 무슨일이 발생할까?
post 엔티티는 총 3개의 엔티티와 연관관계를 맺고 있고(postImage, postLike, comment) 각 casacde 옵션이 (ALL, REMOVE, REMOVE) 이다.
즉 post 삭제 시 연쇄적으로 삭제하고자 하는 post 엔티티와 연관관계를 맺고있는 모든 엔티티가 삭제된다.
그럼 post 와 직접적으로 연관관계를 맺고 있는데 postImage, postLike, comment 만 삭제가 될까?
아니다!
comment 엔티티 필드를 잘보면 commentLikeList 에 cascade = CascadeType.REMOVE 옵션이 설정되어 있는 것을 확인할 수 있다.
post 엔티티 삭제 시 프로세스를 살펴보자
- post 삭제 전 연관된 (postImage, postLike, comment) 삭제
- post 와 연관된 comment 삭제 전 commentLike 엔티티 삭제
- 최종적으로 삭제하고자 하는 Post 삭제
부모엔티티 먼저 삭제하면 자식엔티티의 부모엔티티를 참조할 수 없기 때문이다.
따라서 자식 엔티티들의 cascade 옵션을 REMOVE 또는 ALL 로 설정할 경우 삭제하고자 하는 엔티티의 연관관계의 끝에서 부터 삭제가 된다.