GuestBook 프로젝트 (6) (Member / Board / Reply 생성, 연관 관계 추가, 지연(Lazy) / 즉시(Eager) 로딩, Fetch)
GuestBook 프로젝트 (6) (Member / Board / Reply 생성, 연관 관계 추가, 지연(Lazy) / 즉시(Eager) 로딩, Fetch)
GuestBook 프로젝트 (5) (검색 처리, 검색 조건 추가, 수정 및 삭제 후 redirect) GuestBook 프로젝트 (5) (검색 처리, 검색 조건 추가, 수정 및 삭제 후 redirect) GuestBook 프로젝트 (4) (guestbook 등록, 조회, 수정,
soohykeee.tistory.com
연관관계가 없는 엔티티 조인 처리에는 ON
Board와 Member 사이에는 내부적으로 참조를 통해서 연관관계가 있지만, Board와 Reply는 상황이 좀 다르다. Reply쪽에서 @ManyToOne으로 참조하고 있으나 Board입장에서는 Reply 객체들을 참조하고 있지 않기 때문에 문제가 된다. 이러한 경우에는 직접조인이 필요한 조건에 'JOIN ON'을 이용하여서 작성해 줘야한다.
특정 게시물과 해당 게시물에 속한 댓글들을 조회해야하는 상황을 생각하면 Board와 Reply 테이블을 조인해서 쿼리를 작성하게 된다.
앞서 말한 해당 쿼리를 BoardRepository에 적용시키면 아래와 같다.
package com.example.guestbookdemo.repository;
import com.example.guestbookdemo.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface BoardRepository extends JpaRepository<Board, Long> {
@Query("select b,w from Board b left join b.writer w where b.bno =:bno")
Object[] getBoardWithWriter(@Param("bno") Long bno);
@Query("select b,r from Board b left join Reply r on r.board = b where b.bno=:bno")
List<Object[]> getBoardWithReply(@Param("bno") Long bno);
}
게시판 목록을 위한 JPQL
게시판 목록에서 필요한 데이터는 아래와 같다.
- 게시물(Board) : 게시물의 번호, 제목, 게시물의 작성 시간
- 회원(Member) : 회원의 이름, 이메일
- 댓글(Reply) : 해당 게시물의 댓글 수
위의 entity중에서 가장 많은 데이터를 가져오는 곳은 Board 이다. 해당 엔티티를 중심으로 JPQL 설계가 필요하다. 목록화면에 뿌려줄 JPQL, 조회 시에 필요한 데이터를 뿌려줄 JPQL을 작성하면 아래와 같다.
package com.example.guestbookdemo.repository;
import com.example.guestbookdemo.entity.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface BoardRepository extends JpaRepository<Board, Long> {
//... 생략
// 목록 화면에 필요한 데이터들
@Query(value = "select b, w, count(r) " +
" from Board b " +
" left join b.writer w " +
" left join Reply r on r.board = b " +
" group by b",
countQuery = "select count(b) from Board b")
Page<Object[]> getBoardWithReplyCount(Pageable pageable);
// 조회 화면에 필요한 데이터들
@Query(value = "select b, w, count(r) " +
" from Board b left join b.writer w " +
" left outer join Reply r on r.board = b " +
" where b.bno =:bno",
countQuery = "select count(b) from Board b")
Object getBoardByBno(@Param("bno") Long bno);
}
프로젝트에 적용
프로젝트에 적용시키기 위해서 BoardDTO를 생성해주었다.
package com.example.guestbookdemo.dto;
import lombok.*;
import java.time.LocalDateTime;
@Data
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {
private Long bno;
private String title;
private String content;
private String writerEmail;
private String writerName;
private LocalDateTime regDate;
private LocalDateTime modDage;
private int replyCount;
}
BoardDTO와 Board 엔티티가 조금 다른 점은 화면에 작성자의 이름, 이메일, 댓글의 수를 보여줘야 하므로 해당 속성을 담을 수 있는 writerEmail, writerName, replyCount 변수를 생성했다.
BoardService, BoardServiceImpl은 앞서 GuestbookService에 작성했을때와 동일하게 dto -> entity 해주는 default 메소드와 등록하는 register 메소드를 생성해준다. 아래 링크로 들어가면 설명이 나와있다.
이제 해당 기능들이 정상적으로 돌아가는지 Test코드를 통해 확인해보겠다.
GuestBook 프로젝트 (2) (Querydsl Test Code, DTO, Service, ServiceImpl)
package com.example.guestbookdemo.service;
import com.example.guestbookdemo.dto.BoardDTO;
import com.example.guestbookdemo.entity.Board;
import com.example.guestbookdemo.entity.Member;
public interface BoardService {
Long register(BoardDTO dto);
default Board dtoToEntity(BoardDTO dto) {
Member member = Member.builder().email(dto.getWriterEmail()).build();
Board board = Board.builder()
.bno(dto.getBno())
.title(dto.getTitle())
.content(dto.getContent())
.writer(member)
.build();
return board;
}
}
package com.example.guestbookdemo.service;
import com.example.guestbookdemo.dto.BoardDTO;
import com.example.guestbookdemo.entity.Board;
import com.example.guestbookdemo.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService {
private final BoardRepository boardRepository;
@Override
public Long register(BoardDTO dto) {
Board board = dtoToEntity(dto);
boardRepository.save(board);
return board.getBno();
}
}
Test
등록이 정상적으로 실행이 되는지 해당 Test 코드를 작성한 후 실행해보면 오류없이 db에 등록이 되는 것을 볼 수 있다.
package com.example.guestbookdemo.service;
import com.example.guestbookdemo.dto.BoardDTO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class BoardServiceTests {
@Autowired
private BoardService boardService;
@Test
public void testRegister() {
BoardDTO dto = BoardDTO.builder()
.title("Test - Title")
.content("Test... Content")
.writerEmail("user55@aaa.com") //DB에 존재하는 email 값으로 해야 에러가 나지 않음
.build();
Long bno = boardService.register(dto);
}
}
게시물 목록 처리
게시물 목록 처리의 경우 앞서 만들었던 PageRequestDTO, PageReusltDTO를 사용해야 한다.
PageResultDTO의 핵심은 JPQL의 결과로 나오는 Object[]를 DTO 타입으로 변환하는 기능이다. PageResultDTO의 경우는 화면에 뿌려주는 역할 이기에 DTO로 리턴해줘야 한다. Object[]의 내용은 Board, Member, Long 타입으로 나오게 되는데, 해당 파라미터들을 전달받아서 BoardDTO로 리턴될수 있도록 작성해야 한다.
해당 기능은 BoardService 쪽에 dtoToEntity() 처럼 entity를 dto로 변환해주는 entityToDTO() 메소드를 작성해줘야 한다.
그리고 dto로 전달받은 값들을 목록으로 처리해주기 위해서 getList 메소드를 작성해줘야 한다.
package com.example.guestbookdemo.service;
import com.example.guestbookdemo.dto.BoardDTO;
import com.example.guestbookdemo.dto.PageRequestDTO;
import com.example.guestbookdemo.dto.PageResultDTO;
import com.example.guestbookdemo.entity.Board;
import com.example.guestbookdemo.entity.Member;
public interface BoardService {
Long register(BoardDTO dto);
PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO);
default Board dtoToEntity(BoardDTO dto) {
//... 생략
}
default BoardDTO entityToDTO(Board board, Member member, Long replyCount) {
BoardDTO boardDTO = BoardDTO.builder()
.bno(board.getBno())
.title(board.getTitle())
.content(board.getContent())
.regDate(board.getRegDate())
.modDage(board.getModDate())
.writerEmail(member.getEmail())
.writerName(member.getName())
.replyCount(replyCount.intValue())
.build();
return boardDTO;
}
}
package com.example.guestbookdemo.service;
import com.example.guestbookdemo.dto.BoardDTO;
import com.example.guestbookdemo.dto.PageRequestDTO;
import com.example.guestbookdemo.dto.PageResultDTO;
import com.example.guestbookdemo.entity.Board;
import com.example.guestbookdemo.entity.Member;
import com.example.guestbookdemo.repository.BoardRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
@Log4j2
public class BoardServiceImpl implements BoardService {
private final BoardRepository boardRepository;
@Override
public Long register(BoardDTO dto) {
//... 생략
}
@Override
public PageResultDTO<BoardDTO, Object[]> getList(PageRequestDTO pageRequestDTO) {
Function<Object[], BoardDTO> fn = (en -> entityToDTO((Board) en[0], (Member) en[1], (Long) en[2]));
Page<Object[]> result = boardRepository.getBoardWithReplyCount(pageRequestDTO.getPageable(Sort.by("bno").descending()));
return new PageResultDTO<>(result, fn);
}
}