
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상에선 보지 못함
}
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)
}



@ManyToOne, @OneToMany의 dafault// default값
@ManyToOne(fetch = FetchType.EAGER)
@OneToMany, @ManyToMany의 dafault// default값
@OneToMany(fetch = FetchType.LAZY)
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")
}
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();
}
}
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";
}
}
<!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인데 어떻게 리뷰가 넘어올까?
왜 ?
- Open Session in View(OSIV) : default값 true (생략가능)
open-in-view: true이기 때문에 session이 DB와 연결되어 있어서 리뷰가 넘어올 수 있는 것이다.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
-> 이를 해결하기 위해선 두가지 방법이 있음
fetch = FetchType.EAGER로 변경하면 가능하나 성능 저하 문제로 실무에서는 LAZY를 사용함. // 책 목록 가져오기
@Transactional(readOnly = true)
public List<Book> getAllBooks(){
List<Book> books = bookRepository.findAll();
books.forEach(book -> Hibernate.initialize(book.getReviews()));
return books;
}
@Query("SELECT B FROM Book B LEFT JOIN FETCH B.reviews")
public List<Book> findAllWithReviews();
// 책 목록 가져오기
@Transactional(readOnly = true)
public List<Book> getAllBooks(){
return bookRepository.findAllWithReviews();
}
@EntityGraph(attributePaths = {"reviews"})
@Query("SELECT B FROM Book B")
public List<Book> findAllWithReviews();