1. spring.jpa.open-in-view
- 스프링 부트에서 JPA를 사용할 때의 설정 중 하나
- 기본적으로 활성화 : view까지 끌고 옴
- HTTP 요청이 완료될 때까지 영속성 컨텍스트를 열어두는 역할
영속성 컨텍스트
엔티티 객체를 데이터베이스 테이블과 매핑해주는데 사용되는 캐시와 유사한 개념
트랜잭션 내에서 엔티티 객체의 상태를 추, 엔티티 객체의 변경을 데이터베이스에 반영
- 활성화 상태 : 영속성 컨텍스트가 HTTP 요청의 끝까지 유지
뷰 렌더링 동안에도 영속성 컨텍스트가 열려 있는 상태를 유지, 지연로딩(Lazy Loading)이 가능
지연로딩
JPA에서 연관된 엔티티를 실제로 사용하기 직전까지 데이터베이스에서 로딩을 지연
성능을 향상시키고 메모리 사용을 최적화할 수 있도록 도와줌
발생하는 시점이 영속성 컨텍스트가 열려 있는 동안에만 가능
영속성 컨텍스트를 종료하기 전까지 지연로딩된 엔티티를 접근하는 것이 중요
- 비활성화 : sevice가 종료될 때 끝남
2. 목록 보기
- 댓글 3개, board 정보(제목, 내용), user정보(username)의 관계 : N대 1대 1
3. 댓글 정보 추가하기 - 전략
- user는 one 관계 - > join
board는 many 관계 -> 조회 1번 더하기 -> 객체 2개 -> DTO에 담기
- many 관계 -> 양방향 맵핑
3가지 전략
- One 관계는 조인, Many 관계는 Lazy Loading
- One 관계는 조인,
Many 관계를 페이징해야 된다면, 직접 쿼리를 만들어서 두 번 조회
- 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. 양방향 매핑하기
- 컬렉션, 객체 타입은 필드화 될 수 없음
- 역방향의 필드를 적어서 조회할 때 담는 용도로만 사용하기!
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 } }
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); } }
- 현재 테이블 현황
- 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"; } }
- 커넥션을 끈지 않고 뷰 랜더링할 때 까지 커넥션을 유지함 → Lazyloarding 가능
Share article