inblog logo
|
vosw1
    Spring

    37. 댓글 목록 보기v6

    송민경's avatar
    송민경
    Mar 19, 2024
    37. 댓글 목록 보기v6

    1. spring.jpa.open-in-view

    • 스프링 부트에서 JPA를 사용할 때의 설정 중 하나
    • 기본적으로 활성화 : view까지 끌고 옴
    • HTTP 요청이 완료될 때까지 영속성 컨텍스트를 열어두는 역할
    영속성 컨텍스트
    엔티티 객체를 데이터베이스 테이블과 매핑해주는데 사용되는 캐시와 유사한 개념
    트랜잭션 내에서 엔티티 객체의 상태를 추, 엔티티 객체의 변경을 데이터베이스에 반영
    • 활성화 상태 : 영속성 컨텍스트가 HTTP 요청의 끝까지 유지
    뷰 렌더링 동안에도 영속성 컨텍스트가 열려 있는 상태를 유지, 지연로딩(Lazy Loading)이 가능
    지연로딩
    JPA에서 연관된 엔티티를 실제로 사용하기 직전까지 데이터베이스에서 로딩을 지연
    성능을 향상시키고 메모리 사용을 최적화할 수 있도록 도와줌
    발생하는 시점이 영속성 컨텍스트가 열려 있는 동안에만 가능
    영속성 컨텍스트를 종료하기 전까지 지연로딩된 엔티티를 접근하는 것이 중요
    • 비활성화 : sevice가 종료될 때 끝남
    notion image
     

    2. 목록 보기

    • 댓글 3개, board 정보(제목, 내용), user정보(username)의 관계 : N대 1대 1
     

    3. 댓글 정보 추가하기 - 전략

    • user는 one 관계 - > join
    board는 many 관계 -> 조회 1번 더하기 -> 객체 2개 -> DTO에 담기
    • many 관계 -> 양방향 맵핑
    💡
    3가지 전략
    1. One 관계는 조인, Many 관계는 Lazy Loading
    1. One 관계는 조인,
    Many 관계를 페이징해야 된다면, 직접 쿼리를 만들어서 두 번 조회
    1. One 관계와 Many 관계를 한번에 조인
    • 단위 테스트하기
    package shop.mtcoding.blog.reply; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import java.util.List; /* 2가지 전략 * 1. user는 one 관계 - > join * board는 many 관계 -> 조회 1번 더하기 -> 객체 2개 -> DTO에 담기 * 2. many 관계 -> 양방향 맵핑 * */ @DataJpaTest public class ReplyJPARepositoryTest { @Autowired private ReplyJPARepository replyJPARepository; @Test public void findByBoardId_test() { // given int boardId = 4; // when List<Reply> replyList = replyJPARepository.findByBoardId(boardId); // then System.out.println(replyList.size()); } }
     

    4. 댓글로 페이징하는 방법

    • 댓글 페이징 → join해서 하는 쿼리가 매우 힘듦
    댓글 페이징 쿼리를 따로 짜서 조회를 따로해서 DTO로 합치기
     

    5. 양방향 매핑하기

    notion image
    • 컬렉션, 객체 타입은 필드화 될 수 없음
    • 역방향의 필드를 적어서 조회할 때 담는 용도로만 사용하기!
    board와 user join해서 한번 담고 reply 조회해서 한번 담기
    • @OneToMany : 반대 방향
    (mappedBy = "board") : 필드화될 수 없으니까 외래키의 주인(entity 객체)의 필드명 알려주기
    • 기본 디폴트 설정 Many To One : EAGER
    One To Many : Lazy
    package shop.mtcoding.blog.board; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIncludeProperties; import jakarta.persistence.*; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.CreationTimestamp; import shop.mtcoding.blog.reply.Reply; import shop.mtcoding.blog.user.User; import shop.mtcoding.blog._core.utils.MyDateUtil; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; @NoArgsConstructor @Data // 변경되는 데이터에만 setter가 필요함 @Table(name = "board_tb") @Entity public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private String content; // private String username; // username은 user_tb에도 있기에 외래키로 연관관계 맺어야 함 // @JoinColumn(name = "user_id") 직접 지정할 때 @ManyToOne(fetch = FetchType.LAZY) // ORM 할 것이기에 user 객체 필요 private User user; // DB에 컬럼명 : user_id (변수명_PK키) @CreationTimestamp // PC로 인해 DB에 INSERT될 때 날짜 주입 private Timestamp createdAt; @OneToMany(mappedBy = "board") // 반대방향 -> 필드화될 수 없으니까 외래키의 주인(entity 객체)의 필드명 알려주기 private List<Reply> replies = new ArrayList<>(); // 테이블은 컬렉션이 필드화될 수 없어서 오류가 남 @Transient // 테이블 생성이 안됨 private boolean isOwner; public void update(){ } @Builder public Board(Integer id, String title, String content, User user, Timestamp createdAt) { this.id = id; this.title = title; this.content = content; this.user = user; this.createdAt = createdAt; } public String getBoardDate(){ return MyDateUtil.timestampFormat(createdAt); } }
     
    package shop.mtcoding.blog.Board; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.data.domain.Sort; import shop.mtcoding.blog.board.Board; import shop.mtcoding.blog.board.BoardJPARepository; import shop.mtcoding.blog.user.User; import java.util.List; import java.util.Optional; @DataJpaTest public class BoardJPARepositoryTest { @Autowired private BoardJPARepository boardJPARepository; // findByIdJoinUser @Test public void findByIdJoinUser_test() { // given int id = 1; // when Optional<Board> board = boardJPARepository.findByIdJoinUser(id); // then } }
    notion image
     
    package shop.mtcoding.blog.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.Optional; public interface BoardJPARepository extends JpaRepository<Board, Integer> { @Query("select b from Board b join fetch b.user u where b.id = :id") Optional<Board> findByIdJoinUser(@Param("id") int id); @Query("select b from Board b join fetch b.user u join fetch b.replies r where b.id = :id") Optional<Board> findByIdJoinUserAndReply(@Param("id") int id); }
    • 단위 테스트하기
    package shop.mtcoding.blog.Board; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.data.domain.Sort; import shop.mtcoding.blog.board.Board; import shop.mtcoding.blog.board.BoardJPARepository; import shop.mtcoding.blog.user.User; import java.util.List; import java.util.Optional; @DataJpaTest public class BoardJPARepositoryTest { @Autowired private BoardJPARepository boardJPARepository; @Test public void findByIdJoinUserAndReplies_test() { // given int id = 4; // when Optional<Board> board = boardJPARepository.findByIdJoinUserAndReplies(id); // then System.out.println("findByIdJoinUser_test : " + board); } }
    notion image
     
    • 현재 테이블 현황
    • id(PK)는 다 index
    User
    ㅤ
    userId
    username
    1
    ssar
    2
    cos
    3
    love
    Board
    ㅤ
    ㅤ
    boardId
    title
    content
    1
    제목1
    내용1
    2
    제목2
    내용2
    3
    제목3
    내용3
    4
    제목4
    내용4
    Reply
    ㅤ
    ㅤ
    ㅤ
    replyId
    comment
    userId
    boardId
    1
    댓글1
    4
    1
    2
    댓글2
    4
    1
    3
    댓글3
    4
    2
    4
    댓글4
    3
    2
     

    6. 조회 2번 하는 것부터 시작

    • 드라이빙 : board , 드리븐 : user
    • findByIdJoinUser 쿼리
    @Query("select b from Board b join fetch b.user u where b.id = :id") Optional<Board> findByIdJoinUser(@Param("id") int id);
    b.id
    b.title
    b.user
    u.id
    u.username
    ㅤ
    조회수
    1
    제목1
    1
    1
    ssar
    ㅤ
    2
    2
    제목2
    1
    1
    ssar
    ㅤ
    2
    3
    제목3
    2
    2
    cos
    ㅤ
    2
    4
    제목4
    3
    3
    love
    ㅤ
    2
     

    7. 한 번에 JOIN

    • join한 결과를 가지고 다시 join하는 것
    @Query("select b from Board b join fetch b.user u left join fetch b.replies r where b.id = :id") Optional<Board> findByIdJoinUserAndReplies(@Param("id") int id);
    b.id
    b.title
    b.user
    u.id
    u.username
    ㅤ
    조회수
    1
    제목1
    1
    1
    ssar
    ㅤ
    2
    2
    제목2
    1
    1
    ssar
    ㅤ
    2
    3
    제목3
    2
    2
    cos
    ㅤ
    2
    4
    제목4
    3
    3
    love
    ㅤ
    2
    Reply
    ㅤ
    ㅤ
    ㅤ
    replyId
    comment
    userId
    boardId
    1
    댓글1
    4
    1
    2
    댓글2
    4
    1
    3
    댓글3
    4
    2
    4
    댓글4
    3
    2
    b.id
    b.title
    b.user
    u.id
    u.username
    ㅤ
    조회수
    r.id
    r.comment
    r.userId
    r.boardId
    조회수
    1
    제목1
    1
    1
    ssar
    ㅤ
    2
    ㅤ
    ㅤ
    ㅤ
    ㅤ
    5
    2
    제목2
    1
    1
    ssar
    ㅤ
    2
    ㅤ
    ㅤ
    ㅤ
    ㅤ
    5
    3
    제목3
    2
    2
    cos
    ㅤ
    2
    4
    댓글4
    3
    2
    5
    4
    제목4
    3
    3
    love
    ㅤ
    2
    1
    댓글1
    4
    1
    5
    4
    제목4
    3
    3
    love
    ㅤ
    2
    2
    댓글2
    4
    1
    ㅤ
    4
    제목4
    3
    3
    love
    ㅤ
    2
    3
    댓글3
    4
    2
    ㅤ
    • 결과
    • join 28번 // 최악!
    • 드라이빙 테이블 변경 16번
    • where 먼저 걸고 join 12번 , 액세스 1번 // 베스트!
    JPA로 안됨, 네이티브 쿼리로 해야 함
    @Query("select r from Reply r right join (select b from Board b join fetch b.user u left join fetch b.replies r where b.id = :id)") Optional<Board> findByIdJoinUserAndRepliesv2(@Param("id") int id);
    • select 2번 12번, 액세스 2번
    유지 보수가 편함
     
    {{> /layout/header}} <div class="container p-5"> {{#isOwner}} <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <!-- Post 요청-> 해당 페이지로 이동--> <a href="/board/{{board.id}}/update-form" class="btn btn-warning me-1">수정</a> <!-- Post 요청 -> 해당 페이지 삭제 --> <form action="/board/{{board.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{board.user.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{board.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{board.content}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> {{#board.replies}} <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">{{user.username}}</div> <!--lazyloarding이 일어나면서 user만 selecting 됨--> <div>{{comment}}</div> </div> <form action="/reply/{{id}}/delete" method="post"> <button class="btn">🗑</button> </form> </div> {{/board.replies}} </div> </div> </div> {{> /layout/footer}}
     
    package shop.mtcoding.blog.board; import ch.qos.logback.core.model.Model; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import shop.mtcoding.blog._core.errors.exception.Exception403; import shop.mtcoding.blog._core.errors.exception.Exception404; import shop.mtcoding.blog.user.User; import java.lang.annotation.Native; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; private final BoardService boardService; // @Transactional 트랜잭션 시간이 너무 길어져서 service에 넣어야함 @PostMapping("/board/{id}/update") public String update(@PathVariable Integer id, BoardRequest.UpdateDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.update(id, sessionUser.getId(), reqDTO); return "redirect:/board/" + id; } @GetMapping("/board/{id}/update-form") public String updateForm(@PathVariable(name = "id") Integer id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); Board board = boardService.updateForm(id, sessionUser.getId()); request.setAttribute("board", board); return "/board/update-form"; // 서버가 내부적으로 index를 요청 - 외부에서는 다이렉트 접근이 안됨 } @PostMapping("/board/{id}/delete") public String delete(@PathVariable Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.delete(id, sessionUser.getId()); return "redirect:/"; } @GetMapping("/") public String index(HttpServletRequest request) { List<Board> boardList = boardService.findAll(); request.setAttribute("boardList", boardList); return "index"; // 서버가 내부적으로 index를 요청 - 외부에서는 다이렉트 접근이 안됨 } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO reqDTO) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.save(reqDTO, sessionUser); return "redirect:/"; } @GetMapping("/board/save-form") public String saveForm() { return "board/save-form"; } @GetMapping("/board/{id}") public String detail(@PathVariable Integer id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); Board board = boardService.detail(id, sessionUser); request.setAttribute("board", board); System.out.println("서버 사이드 랜더링 직전에는 board와 user만 조회된 상태이다...이 로고가 찍힌 후 Lazyloarding이 이뤄짐"); return "board/detail"; } }
    notion image
    • 커넥션을 끈지 않고 뷰 랜더링할 때 까지 커넥션을 유지함 → Lazyloarding 가능
    notion image
    notion image
    notion image
    notion image
    Share article

    vosw1

    RSS·Powered by Inblog