ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코드로 배우는 스프링부트 웹 프로젝트 Day 14
    Spring/코드로 배우는 스프링부트 웹 프로젝트 2022. 9. 14. 13:19

    /* Annotation들은 스프링부트 프로젝트의 Annotation 정리 페이지에 따로 정리해두었습니다. */

     

    [ JPQL과 left (outer) join ]

     

    목록 화면에서 게시글의 정보와 함께 댓글의 수를 같이 가져오기 위해서는 단순히 하나의 엔티티 타입을 이용할 수 없음

    => JPQL의 조인(join)을 이용해서 처리

     

    [ left (outer) join ]

    스프링부트 2버전 이후에 포함되는 JPA 버전은 엔티티 클래스 내에 연관관계가 없더라도 조인을 사용할 수 있음

     

    [ 엔티티 클래스 내부에 연관관계가 있는 경우 ]

    Board 엔티티 클래스의 내부에는 Member 엔티티 클래스를 변수로 선언하고, 연관관계를 맺고 있음

    이러한 경우, Board의 writer 변수를 이용해서 조인을 처리함

     

    BoardRepository 클래스 수정

    public interface BoardRepository extends JpaRepository<Board, Long> {
        // 한 개의 로우(Object) 내에 Object[ ]로 나옴
        @Query("select b, w from Board b left join b.writer w where b.bno =:bno")
        Object getBoardWithWriter(@Param("bno") Long bno);
        
    }

    getBoardWithWriter()는 Board를 사용하고 있지만, Member를 같이 조회해야하는 상황

    Board 클래스는 Member와의 연관관계를 맺고 있으므로, b.writer와 같은 형태로 사용함

    이와 같이 내부에 있는 엔티티를 이용할 때는 'LEFT JOIN' 뒤에 'ON'을 이용하는 부분이 없고 'WHERE'을 이용

     

    📎 쿼리문

        • 개별 쿼리마다 동일한 sql문을 수행하되, bno의 값이 바뀜

     

    📎 LEFT JOIN

        • 왼쪽 테이블의 한 개의 레코드에 여러개의 오른쪽 테이블 레코드가 일치할 경우 사용

        • 왼쪽 테이블을 중심으로 오른쪽의 테이블을 매치시킴

        • 왼쪽은 무조건 표시 & 매치되는 레코드가 오른쪽에 없으면 NULL을 표시

     

     

    BoardRepositoryTests 추가

    @Test
    public void testReadWithWriter() {
    
        Object result = boardRepository.getBoardWithWriter(100L);
    
        Object[] arr = (Object[])result;
    
        System.out.println("----------------------------------");
        System.out.println(Arrays.toString(arr));
    
    }

    지연 로딩으로 처리되었으나, 실행되는 쿼리를 보면 join 처리가 되어 한 번에 board테이블과 member테이블을 이용하고 있음

     

    [ 연관관계가 없는 엔티티 조인 처리에는 on ]

    앞서 Board와 Member 사이에는 내부적으로 참조를 통해서 연관관계가 있었음

    하지만, Board와 Reply는 Reply에서 @ManyToOne으로 참조하지만, Board에서 보면, Reply 객체들을 참조하고 있지 않기 때문에 문제가 됨

    => 직접 join에 필요한 조건은 'on'을 이용해서 작성

     

    특정 게시물과 해당 게시물에 속한 댓글들을 조회해야 하는 상황에서는 board와 reply 테이블을 join해서 쿼리를 작성

     

    순수 SQL로 처리하는 경우

    select
                board.bno, board.title, board.writer_email,
                rno, text
    from board left outer join reply
    on reply.board_bno = board.bno
    where board.bno = 100;

     

    위 쿼리를 JPQL로 처리하는 경우 | BoardRepository 인터페이스 추가

    public interface BoardRepository extends JpaRepository<Board, Long> {
        // 한 개의 로우(Object) 내에 Object[ ]로 나옴
        @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);
    }

    연관관계가 있는 경우와 비교해 보면 중간에 'ON'이 사용되면서 join 조건을 직접 지정하는 부분이 있음

     

    BoardRepositoryTests 클래스 추가

    @Test
    public void testGetBoardWithReply() {
    
        List<Object[]> result = boardRepository.getBoardWithReply(100L);
        
        for (Object[] arr : result) {
            System.out.println(Arrays.toString(arr));
            
        }
    }

     

    [ 목록 화면에 필요한 JPQL만들기 ]

     

    목록 화면에서 필요한 데이터

        • 게시물(Board): 게시물의 번호, 제목, 게시물의 작성 시간

        • 회원(Member): 회원의 이름/이메일

        • 댓글(Reply): 해당 게시물의 댓글 수

     

    가장 많은 데이터를 가져오는 엔티티는 Board이므로 Board를 중심으로 join관계를 작성

     

    Member는 Board 내에 writer라는 필드로 연관관계를 맺고 있고, Reply는 연관관계가 없는 상황

    join 후에는 Board를 기준으로 GROUPBY처리를 해서 하나의 게시물 당 하나의 라인이 될 수 있도록 처리해야 함

     

     

    BoardRepository 인터페이스 추가

    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); // 목록 화면에 필요한 데이터
    }

    b를 그룹화

    countQuery로 쿼리 전체 개수를 받기

     

    BoardRepositoryTests에 정상적으로 JPQL이 동작 가능한지 확인하는 테스트 코드 작성

    @Test
    public void testWithReplyCount() {
    
        Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());
    
        Page<Object[]> result = boardRepository.getBoardWithReplyCount(pageable);
    
        result.get().forEach(row -> {
    
            Object[] arr = (Object[])row;
    
            System.out.println(Arrays.toString(arr));
        });
    }

    1페이지의 데이터(10개)를 정리하는 코드

     

    [ 조회 화면에서 필요한 JPQL 구성하기 ]

     

    조회 화면에서는 Board와 Member를 주로 이용 + 각 게시물 당 몇 개의 댓글을 가지고 있는지 알려주도록 설계

    실제 댓글은 화면에서 주로 Ajax를 이용해서 필요한 순간에 동적으로 데이터를 가져오는 방식이 일반적임

     

    BoardRepository 인터페이스에 추가

    @Query ("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")
    Object getBoardByBno(@Param("bno") Long bno);

    목록 처리와 유사하지만, 특정 게시물 번호를 사용하는 부분이 다름

     

    BoardRepositoryTests 클래스 추가

    @Test
    public void testRead3() {
    
        Object result = boardRepository.getBoardByBno(100L);
    
        Object[] arr = (Object[]) result;
    
        System.out.println(Arrays.toString(arr));
    
    }

    특정 게시물의 번호(100)를 사용해서 데이터를 불러옴

     

     

    [ 프로젝트 적용하기 ]

    [ DTO 계층과 서비스 계층 작성 ]

     

    dto, service 패키지 생성 + BoardDTO 클래스 생성

     

    기본적인 DTO 구성 기준은 화면에 전달하는 데이터이거나, 화면 쪽에 전달되는 데이터

    => 엔티티 클래스의 구성과 일치하지 않는 경우가 많음

     

    BoardDTO 클래스

    package org.zerock.board2.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; // 작성자의 이메일(id)
    
        private String writerName; //작성자의 이름
    
        private LocalDateTime regDate;
    
        private LocalDateTime modDate;
    
        private int replyCount; // 해당 게시글의 댓글 수
        
    }

    Board 엔티티 클래스와 다른 점은 Member를 참조하는 대신에 화면에서 필요한 작성자의 이메일(writerEmail)과 작성자의 이름(writerName)으로 처리하는 것

     

     

Designed by Tistory.