안녕하세요.
오늘은 저번 JPA CRUD에 이어
1 : N 관계를 알아보겠습니다.
✔️ 요구사항 분석
- 게시글 : 댓글 = 1 : N
- 댓글 CRUD
- 게시글이 삭제되면, 연관된 댓글들이 모두 삭제된다.
- 특정 게시글을 조회하면, 연관된 댓글들도 같이 조회된다.
일단 먼저 댓글 엔티티 및 CRUD 기능을 생성하겠습니다.
기존의 게시글 CRUD랑 똑같은데,
다만 연관관계를 설정하는 부분만 추가됩니다.
제일 먼저, Comment 엔티티를 보겠습니다.
[Comment 엔티티]
@Getter
@NoArgsConstructor
@Entity
public class Comments extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comments_id")
private Long id;
@Column(columnDefinition = "TEXT", nullable = false)
private String comment;
@ManyToOne
@JoinColumn(name = "posts_id")
private Posts posts;
@Builder
public Comments(String comment, Posts posts) {
this.comment = comment;
this.posts = posts;
}
public void update(String comment) {
this.comment = comment;
}
}
- String comment : 댓글 내용
- post와 연관관계를 맺어줍니다.
- post 하나당, 여러 개의 댓글을 생성할 수 있으므로 @ManyToOne 관계를 설정합니다.
- Join은 post의 id로 할 것입니다. (FK)
다음은 Post 엔티티에 댓글 관련한 것들을 추가합니다.
[Post 엔티티]
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "posts_id")
private Long id;
...
@OneToMany(mappedBy = "posts", cascade = CascadeType.REMOVE)
private List<Comments> commentsList;
...
}
- Comment와 @OneToMany로 양방향 관계를 맺어줍니다.
- 게시글을 조회할 때, 연관된 댓글을 함께 조회하기 위해 양방향을 맺어줬습니다.
- 그리고 cascade = CascadeType.REMOVE로 영속성 전이(제거) 설정을 합니다.
- 부모 엔티티를 삭제하면, 연관된 자식 엔티티도 함께 삭제됩니다.
- 게시글을 삭제하면, 연관된 댓글들이 함께 삭제되도록 하기 위함입니다.
다음은 CommnetRepository입니다.
[CommnetRepository]
public interface CommentsRepository extends JpaRepository<Comments, Long> {
}
다음은 DTO들을 생성할 겁니다.
총 4개를 만들 건데, 제 기준입니다.
(그냥 DTO 하나로도 되고, ResponseDTO & RequestDTO 두 개로 나눠도 됩니다. 본인 마음대로...)
전 다소 세분화된? DTO를 경험하고 싶어, PostDTO와 마찬가지로 4개로 나눴습니다.
- CommentsSaveRequestDto : Comment 저장 DTO
- CommentsUpdateRequestDto : Comment 수정 DTO
- CommentsResponseDto : Comment 조회 DTO (이건 댓글 단건 조회인데, 필요 있을지 모르겠습니다. 일단 그냥 만들었어요)
- CommentsListResponseDto : 댓글 리스트 조회 (게시글 조회시, 연관된 댓글 리스트를 조회하기 위함)
그럼 처음 CommentsSaveRequestDto부터 보겠습니다.
Post DTO들을 생성 했을 때와 거의 같습니다.
[CommentsSaveRequestDto]
@Getter
@NoArgsConstructor
public class CommentsSaveRequestDto {
private String comment;
private Posts posts;
@Builder
public CommentsSaveRequestDto(String comment, Posts posts) {
this.comment = comment;
this.posts = posts;
}
public Comments toEntity() {
return Comments.builder()
.comment(comment)
.posts(posts)
.build();
}
public void setPosts(Posts posts) {
this.posts = posts;
}
}
다음은 CommentsUpdateRequestDto 입니다.
[CommentsUpdateRequestDto]
@Getter
@NoArgsConstructor
public class CommentsUpdateRequestDto {
private String comment;
@Builder
public CommentsUpdateRequestDto(String comment) {
this.comment = comment;
}
}
다음은 CommentsResponseDto이지만, 당장은 안 쓸 것 같아
마지막 CommentsListResponseDto를 보겠습니다.
[CommentsListResponseDto]
@Getter
public class CommentsListResponseDto {
private Long id;
private Long postsId;
private String comment;
private LocalDateTime modifiedDate;
public CommentsListResponseDto(Comments entity) {
this.id = entity.getId();
this.postsId = entity.getPosts().getId();
this.comment = entity.getComment();
this.modifiedDate = entity.getModifiedDate();
}
}
그리고 PostResponseDto에 추가합니다.
게시물을 조회할 때, 댓글 리스트도 함께 조회하기 위함입니다.
[PostResponseDto]
@Getter
public class PostsResponseDto {
...
private List<CommentsListResponseDto> comments;
public PostsResponseDto(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.content = entity.getContent();
this.author = entity.getAuthor();
// 추가
this.comments = entity.getCommentsList().stream()
.map(CommentsListResponseDto::new)
.collect(Collectors.toList());
}
}
- Comments를 commentListResponseDto 클래스로 해서 엔티티 간 무한 참조를 방지합니다.
- 게시글을 조회할 때 댓글을 함께 조회합니다.
- 댓글을 조회할 때 역시 게시글을 함께 조회합니다.
- 이런 엔티티의 무한 참조를 방지하기 위해 DTO를 리턴하는 것입니다.
- @JsonIgnoreProperties를 하는 방법도 있지만, 별도의 DTO를 만들어 반환하겠습니다.
다음은 CommentService를 보겠습니다.
PostService와 거의 같습니다.
[CommentService]
먼저 Comment 저장입니다.
@RequiredArgsConstructor
@Service
public class CommentsService {
@Autowired
CommentsRepository commentsRepository;
@Autowired
PostsRepository postsRepository;
@Transactional
public Long save(CommentsSaveRequestDto commentsSaveRequestDto, Long postId) {
Posts posts = postsRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. postId = " + postId));
commentsSaveRequestDto.setPosts(posts);
Comments comments = commentsSaveRequestDto.toEntity();
commentsRepository.save(comments);
return comments.getId();
}
}
- postId를 파라미터로 받아, 해당 게시물이 존재하지 않으면 IllegalArgumentException 예외를 발생시킵니다.
- comment를 저장할 때, 관련한 post를 set(저장) 합니다.
다음은 update(수정) 코드입니다.
@RequiredArgsConstructor
@Service
public class CommentsService {
@Autowired
CommentsRepository commentsRepository;
@Autowired
PostsRepository postsRepository;
...
@Transactional
public Long update(Long postId, CommentsUpdateRequestDto commentsUpdateRequestDto, Long commentId) {
Posts posts = postsRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. postId = " + postId));
Comments comments = commentsRepository.findById(commentId)
.orElseThrow(() -> new IllegalArgumentException("해당 댓글이 없습니다. id = " + commentId));
comments.update(commentsUpdateRequestDto.getComment());
return commentId;
}
}
- 마찬가지로, postId를 파라미터로 받아 존재하지 않으면 예외를 발생시킵니다.
- 갑자기 든 의문인데, 굳이 Posts로 받아 저장할 필요가 있나 싶네요.
- 그냥 받지 않고 해도 될 것 같습니다.
- commentId 역시 존재하지 않으면 예외를 발생시킵니다.
다음은 해당 댓글을 조회하는 findById인데,
당장은 안 쓸 것 같아 delete(삭제)로 넘어가겠습니다.
@RequiredArgsConstructor
@Service
public class CommentsService {
@Autowired
CommentsRepository commentsRepository;
@Autowired
PostsRepository postsRepository;
...
@Transactional
public void delete(Long postId, Long commentId) {
Posts posts = postsRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. postId = " + postId));
Comments comments = commentsRepository.findById(commentId)
.orElseThrow(() -> new IllegalArgumentException("해당 댓글이 없습니다. id = " + commentId));
commentsRepository.delete(comments);
}
}
마지막으로 Controller를 생성하겠습니다.
View를 만들지 않고, postman으로 테스트할 것이므로, RestController로 할 겁니다.
[CommentApiController]
@RequiredArgsConstructor
@RestController
public class CommentsApiController {
private final CommentsService commentsService;
/* 댓글 생성 */
@PostMapping("/api/v1/posts/{postId}/comments")
public Long save(@RequestBody CommentsSaveRequestDto commentsSaveRequestDto,
@PathVariable("postId") Long postId) {
return commentsService.save(commentsSaveRequestDto, postId);
}
/* 댓글 수정 */
@PutMapping("/api/v1/posts/{postId}/comments/{commentId}")
public Long update(@PathVariable("postId") Long postId,
@PathVariable("commentId") Long commentId,
@RequestBody CommentsUpdateRequestDto dto) {
return commentsService.update(postId, dto, commentId);
}
/* 댓글 단건 조회 */
@GetMapping("/api/v1/posts/{postId}/comments/{commentId}")
public CommentsResponseDto findById(@PathVariable("postId") Long postId,
@PathVariable("commentId") Long commentId) {
return commentsService.findById(postId, commentId);
}
/* 댓글 삭제 */
@DeleteMapping("/api/v1/posts/{postId}/comments/{commentId}")
public Long delete(@PathVariable("postId") Long postId,
@PathVariable("commentId") Long commentId) {
commentsService.delete(postId, commentId);
return commentId;
}
}
- 댓글 단건 조회는 없어도 됩니다.
- 그냥 습관적으로 만듦...
다 만들었으니 Postman으로 테스트 해봅니다.
TestCode로 테스트 하고 싶지만,
그것도 또하나의 코드를 짜는 것이므로 다음 포스팅으로 넘기겠습니다.
먼저 Post(게시물)을 두 개 생성합니다.
영속성 전이를 테스트하기 위해 여러 개를 만든 겁니다.
잘 생성이 됩니다.
insert 쿼리도 두 번 잘 나갑니다.
다음은 post_id 1번 댓글 2개와
post_id 2번 댓글 1개를 생성합니다.
혹시 몰라 순서는 뒤죽박죽 생성해볼 겁니다.
잘 생성이 됩니다.
쿼리도 잘 나갑니다.
Service에서 postId가 없으면 예외를 발생시키게 했으므로,
postId를 조회하는 select 쿼리도 잘 나갑니다.
다음은 post를 조회해보겠습니다.
연관된 댓글이 함께 조회되면 됩니다.
먼저, post_id가 1인 것을 조회하겠습니다.
잘 조회가 됩니다.
다음은 post_id 2를 조회해보겠습니다.
연관된 댓글 2개가 잘 조회 됩니다.
그리고 저는 index를 post 리스트를 조회하게 했습니다.
하지만, post 단건 조회를 할 땐 연관된 comment를 함께 조회를 하고,
index를 조회할 때는 단순 post들만 조회하도록 했는데, 테스트를 해보겠습니다.
댓글을 제외한 post들만 잘 조회가 됩니다.
다음은 댓글 수정을 해보겠습니다.
잘 수정 되었고, 수정 시간도 잘 업데이트 되었습니다.
다음은 id가 3인 댓글 삭제를 해보겠습니다.
잘 삭제 되었고,
delete 쿼리도 잘 나갑니다.
마지막으로 영속성 전이를 테스트하기 위해
댓글들을 몇개 더 추가해보겠습니다.
추가했습니다.
그리고 post_id가 1인 것을 삭제해보겠습니다.
연관된 comment 3개가 삭제되면 됩니다.
한 번에 모두 삭제가 잘 되었습니다.
delete 쿼리도 3번 잘 나갔습니다.
++ 지금 다시보니까 캡쳐를 잘못한 거 같네요;;
delete comment 3개인 줄 알았는데, post가 하나 껴있네요.
- JPA 1 : N 관계 알아보기 끝 -
'프로젝트' 카테고리의 다른 글
Spring Security로 회원가입을 해보자 (2) - SecurityConfig 및 회원가입 (0) | 2023.03.02 |
---|---|
Spring Security로 회원가입을 해보자 (1) - 회원 엔티티 설계 (0) | 2023.03.01 |
스프링 시큐리티를 분석해보자 (0) | 2023.02.25 |
Session 기반 인증과 Token 기반 인증의 차이가 뭘까? (2) | 2023.02.20 |
Custom Exception 해보기 (0) | 2023.02.19 |