Spring/JPA [15] 책 게시판 4-(1)

totwo·2024년 9월 23일

Spring/JPA

목록 보기
15/17
post-thumbnail

기본 설정

Book

  • entity
package com.example.springboot.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String title;
    private int price;
    private String author;
    private int page;

    // fetch = FetchType.LAZY (default)
    // mappedBy : 연관관계의 주인이 내가 아니다.
    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
    private List<Review> reviews; // 1:N으로 List로 받아옴
    // List나 Set으로 넣어주어야 함 - Set(중복불가), List(중복가능)
    // Object와 Object로 가리키고 있다는 뜻으로 DB상에선 보지 못함
}  

Review

  • entity
package com.example.springboot.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Date;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Review {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int cost;
    private String content;
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt=new Date();

    @ManyToOne // 관계설정 : BOOK-REVIEW가 N:1 관계를 가지고 있다
    // fetch = FetchType.EAGER (default)
    // HEY JPA!!! Book book은 테이블의 컬럼으로 만들지 말고 FK로 만들어다오
    @JoinColumn(name="book_id", referencedColumnName = "id", nullable = false) // FK
    // referencedColumnName = "id" 생략 가능
    private Book book; // book_PK(id)
}

🤍Fetch Type

  • JPA가 Entity를 조회할 때 연관관계에 있는 객체들을 어떻게 가져올 것인지 결정하는 설정값

✅Eager Loading(즉시로딩)

  • 데이터를 조회시 연관된 모든 객체의 데이터를 한 번에 불러오는 것
  • @ManyToOne, @OneToMany의 dafault
// default값
@ManyToOne(fetch = FetchType.EAGER)

✅Lazy Loading(지연로딩)

  • 필요한 시점에 연관된 객체의 대이터를 불러오는 것
  • @OneToMany, @ManyToMany의 dafault
// default값
@OneToMany(fetch = FetchType.LAZY)

BookRepository

  • interface
    repository 안에서 사용할 수 있는
package com.example.springboot.repository;

import com.example.springboot.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BookRepository extends JpaRepository<Book, Long> {

    // 1. JPA에서 제공해주는 메서드(CRUD)

    // 2. Query Method

    // 3. JPQL(SQL을 직접 사용 가능)
    // - SELECT * FROM BOOK WHERE~
    // 4. Querydsl(복잡한 SQL을 메서드 기반으로 사용) - X
    // - select().from().where("id=id")
}

책 목록

BookService

  • service
package com.example.springboot.service;

import com.example.springboot.entity.Book;
import com.example.springboot.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class BookService {

    private final BookRepository bookRepository;
    
    // 책 목록 가져오기
    @Transactional(readOnly = true) // 생략가능
    public List<Book> getAllBooks(){
        return bookRepository.findAll();
    }
} 

BookController

  • controller
package com.example.springboot.controller;

import com.example.springboot.entity.Book;
import com.example.springboot.service.BookService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class BookController {

    private final BookService bookService;

    // 책 목록 가져오기
    @GetMapping("/list")
    public String list(Model model){
        List<Book> books = bookService.getAllBooks();
        model.addAttribute("books", books);
        return "list";
    }
}

list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>list</title>
    <script>
        function goDel(id){
            console.log(id);
            location.href="/delete/"+id;
        }
    </script>
</head>
<body>
<h3>Spring Boot, JPA, Thymeleaf</h3>
<table border="1">
    <tr>
        <th>번호</th>
        <th>제목</th>
        <th>가격</th>
        <th>저자</th>
        <th>페이지</th>
        <th>삭제</th>
        <th>삭제</th>
        <th>리뷰</th>
    </tr>
    <tr th:each="book:${books}">
        <td th:text="${book.id}">ID</td>
        <td><a th:href="@{/detail/{id}(id=${book.id})}" th:text="${book.title}">TITLE</a></td>
        <td th:text="${book.price}">PRICE</td>
        <td th:text="${book.author}">AUTHOR</td>
        <td th:text="${book.page}">PAGE</td>
        <td><button th:onclick="goDel([[${book.id}]])">삭제</button></td>
        <td><a th:href="@{/delete/{id}(id=${book.id})}">삭제</a>
        <td>
          <ul>
              <li th:each="review:${book.reviews}" th:text="${review.content}"></li>
          </ul>
        </td>
    </tr>
</table>
<a th:href="@{/register}">등록</a>
</body>
</html>

book entity의 reviews는 Lazy Loading인데 어떻게 리뷰가 넘어올까?
왜 ?

application.yml

  • Open Session in View(OSIV) : default값 true (생략가능)
  • 기본적으로 open-in-view: true이기 때문에 session이 DB와 연결되어 있어서 리뷰가 넘어올 수 있는 것이다.
  • 그러나 Lazy Loading일 때 OSIV가 true이면 N+1 문제가 발생할 수 있으므로
  • open-in-view: false 로 변경하기

-> false로 설정시 LazyInitializationException 발생함.

  • Caused by: org.attoparser.ParseException: failed to lazily initialize a collection of role: com.example.springboot.entity.Book.reviews: could not initialize proxy - no Session
  • Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.springboot.entity.Book.reviews: could not initialize proxy - no Session

-> 이를 해결하기 위해선 두가지 방법이 있음

🚧 LazyInitializationException

  • fetch = FetchType.EAGER로 변경하면 가능하나 성능 저하 문제로 실무에서는 LAZY를 사용함.
  • 즉시로딩 사용 시 예상하지 못한 SQL문이 많이 생성될 수 있으며 JPQL에서 N+1 문제를 발생시킴.

(1) 서비스 계층에서 reviews를 미리 로드

BookService

    // 책 목록 가져오기
    @Transactional(readOnly = true) 
    public List<Book> getAllBooks(){ 
        List<Book> books = bookRepository.findAll();
        books.forEach(book -> Hibernate.initialize(book.getReviews())); 
        return books;
    }

(2) JPQL을 이용한 Fetch Join

BookRepository

    @Query("SELECT B FROM Book B LEFT JOIN FETCH B.reviews")
    public List<Book> findAllWithReviews();

BookService

    // 책 목록 가져오기
    @Transactional(readOnly = true)  
    public List<Book> getAllBooks(){ 
        return bookRepository.findAllWithReviews();
    }

(3) Entity Graph

BookRepository

    @EntityGraph(attributePaths = {"reviews"})
    @Query("SELECT B FROM Book B")
    public List<Book> findAllWithReviews();
profile
Hello, World!

0개의 댓글