# 도서 리뷰 등록하기
각 도서에 리뷰를 등록하는 기능을 구현한다. 이를 위해 일대다 관계에 대한 학습이 필요하다.
## 목차
1. 일대다 관계란?
2. 실습하기: 도서별 리뷰작성
- View: 폼 만들기
- Controller: 폼 데이터 받기 -> 맵퍼 호출
- Model: VO 생성 -> Mapper 등록
- DB: 테이블 생성
3. 연습문제: 음식별 재료등록
## One-to-Many 관계란?
One-to-Many 관계란 데이터베이스에서 사용하는 개념으로, 하나의 row가 다른 테이블의 여러가지 row들과 연결성을 갖는 경우를 뜻한다.
![Imgur](https://i.imgur.com/ulQ1O7p.png)
온라인 음식 주문 사이트를 예로 들어보자. 하나의 음식(`foods`)은 여러가지 재료(`ingredients`)들로 구성될 수 있다. 이 경우 음식-재료의 관계는 `one-to-many` 관계가 된다.
## 실습하기: 도서별 리뷰 생성
일대다 관계를 적용하여 각 도서별로 리뷰를 작성해보자.
### 기존 도서 상세 페이지
![Imgur](http://i.imgur.com/4BZvKgF.png)
**views/books/show.jsp**
```jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="f" %>
<%@ page pageEncoding="utf-8"%>
<div class="jumbotron">
<h1>${ book.title }</h1>
<p>${ book.author } 저</p>
</div>
<div class="thumbnail">
<img src="${ book.image }">
</div>
```
### 폼 생성
리뷰를 등록 할 수 있도록 아래와 같은 폼을 만들자.
![Imgur](http://i.imgur.com/Jn3dfVJ.png)
```jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="f" %>
<%@ page pageEncoding="utf-8"%>
<div class="jumbotron">
<h1>${ book.title }</h1>
<p>${ book.author } 저</p>
</div>
<div class="thumbnail">
<img src="${ book.image }">
</div>
<div class="page-header">
<h2>리뷰</h2>
</div>
<c:url var="reviewsPath" value="/reviews" />
<f:form modelAttribute="review" action="${ reviewsPath }" method="post">
<c:forEach var="error" items="${ fieldErrors }">
<div class="alert alert-warning">
<strong>${ error.getField() }</strong>: ${ error.getDefaultMessage() }
</div>
</c:forEach>
<f:textarea path="text" cssClass="form-control" rows="5" />
<f:hidden path="bookId" />
<f:hidden path="userId" />
<button class="btn btn-block btn-primary" type="submit">리뷰 등록</button>
</f:form>
```
하지만 이게 왠걸.. 분명 에러가 날 것이다. 아래처럼..
![Imgur](http://i.imgur.com/EK0rDgc.png)
해당 에러는 새롭게 추가한 스프링 폼태그 라이브러리에서 리뷰객체를 찾을 수 없기때문에 생기는 문제이다. 따라서 해당 컨트롤러의 메소드에 리뷰객체를 생성하고 모델에 등록을 해주어야 한다.
**BooksController.java**
```java
...
@Autowired
private UserMapper userMapper;
...
@RequestMapping(value = "/books/{id}", method = RequestMethod.GET)
public String show(@PathVariable int id, Model model, Principal principal) {
Book book = bookMapper.getBook(id);
model.addAttribute("book", book);
// 폼 태그에서 modelAttribute="review" 속성을 읽어올 수 있어야함.
Review review = new Review();
review.setBookId(id);
String email = principal.getName();
int userId = userMapper.getUserIdByEmail(email);
review.setUserId(userId);
model.addAttribute("review", review);
return "books/show";
}
```
추가로 현재 유저의 id 값을 가져올 수 있어야 한다.
UserMapper.java
```
@Select("select id from users where email = #{email}")
public int getUserIdByEmail(String email);
```
이제 VO 객체인 Review 클래스를 만들어줘야 겠다.
**Review.java**
```java
public class Review {
Integer id;
String text;
Integer bookId;
Integer userId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Integer getBookId() {
return bookId;
}
public void setBookId(Integer bookId) {
this.bookId = bookId;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
@Override
public String toString() {
return "Review [id=" + id + ", text=" + text + ", bookId=" + bookId + ", userId=" + userId + "]";
}
}
```
### 폼 데이터 전송
리뷰 등록 폼에서 리뷰를 던져보도록 하자.
![Imgur](http://i.imgur.com/E7YKFx0.png)
전송될 위치는 `action` 속성에 적혀있는 url이다.
**views/books/show.jsp**
```jsp
...
<c:url var="reviewsPath" value="/reviews" />
<f:form modelAttribute="review" action="${ reviewsPath }" method="post">
...
```
역시나 에러가 난다.
![Imgur](http://i.imgur.com/cjhckvu.png)
### 컨트롤러 생성
위에서 생긴 에러는 해당 url 요청을 처리해줄 컨트롤러가 존재하지 않았기 때문에 생기는 것이다. 따라서 ReviewsController를 만들어 주어 이를 해결하자.
**ReviewsController.java**
```java
@Controller
public class ReviewsController {
@ResponseBody
@RequestMapping(value = "/reviews", method = RequestMethod.POST)
public String create(@ModelAttribute Review review) {
return review.toString();
}
}
```
이제 다시 폼 데이터를 던져볼까??
![Imgur](http://i.imgur.com/E7YKFx0.png)
음.. 한글이 깨지긴 했으나.. 잘 던져지는 듯 하다..
![Imgur](http://i.imgur.com/qvwE96K.png)
혹시모르니 콘솔창에서도 찍어보았다.
**ReviewsController.java**
```java
@Controller
public class ReviewsController {
@ResponseBody
@RequestMapping(value = "/reviews", method = RequestMethod.POST)
public String create(@ModelAttribute Review review) {
System.out.println(review);
return review.toString();
}
}
```
데이터가 잘 넘어왔음을 확인했다.
![Imgur](http://i.imgur.com/OzN846Z.png)
### 맵퍼 등록
받아온 데이터를 DB로 저장해야 한다. 어떻게 해야할까? 맵퍼를 만들어 처리하자.
**ReviewsController.java**
```java
@Controller
public class ReviewsController {
@Autowired
private ReviewMapper reviewMapper;
@RequestMapping(value = "/reviews", method = RequestMethod.POST)
public String create(@ModelAttribute Review review) {
reviewMapper.create(review);
return "redirect:/books/" + review.getBookId();
}
}
```
위 코드가 컴파일 될 수 있도록 먼저 ReviewMapper 인터페이스를 만들어주자.
**ReviewMapper.java**
```java
public interface ReviewMapper {
@Insert("INSERT INTO reviews (text, book_id, user_id) VALUES (#{text}, #{bookId}, #{userId})")
void create(Review review);
}
```
맵퍼를 만들었다면 이제 root-context.xml에 등록해야한다.
root-context.xml
```xml
...
<bean id="reviewMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.mycompany.mapper.ReviewMapper" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
...
```
xml을 바꾸었다면, 서버 재시작은 바로바로 해주자.
### 리뷰 등록 DB 확인
리뷰 테이블을 만들어 준다.
```
create table reviews (
id serial primary key,
text text,
book_id integer,
);
```
현재 DB에는 book_id 값이 2인 리뷰는 하나도 없다.
![Imgur](http://i.imgur.com/GM58sbx.png)
최종적으로 DB에 데이터가 들어가지는지 확인 해보자.
![](http://i.imgur.com/E7YKFx0.png)
리뷰 폼을 전송하니 페이지가 리다이렉팅 되었다.
![Imgur](http://i.imgur.com/4BZvKgF.png)
DB를 확인해보면 데이터가 잘 들어왔음을 알 수 있다!!
![Imgur](http://i.imgur.com/kpVP7tQ.png)
### 리뷰 데이터 가져오기
이제 리뷰등록기능이 구현 되었으니, 등록된 리뷰를 볼 수 있어야 겠다. 리뷰 맵퍼를 등록하고 이를 통해 해당 리뷰들을 가져올 수 있도록 한다.
**BooksController.java**
```java
@Controller
public class BooksController {
...
@Autowired
private ReviewMapper reviewMapper;
...
@RequestMapping(value = "/books/{id}", method = RequestMethod.GET)
public String show(@PathVariable int id, Model model, Principal principal) {
Book book = bookMapper.getBook(id);
model.addAttribute("book", book);
// 기존 리뷰들
List<Review> reviews = reviewMapper.getReviews(id);
model.addAttribute("reviews", reviews);
// 폼 태그에서 modelAttribute="review" 속성을 읽어올 수 있어야함.
Review review = new Review();
review.setBookId(id);
String email = principal.getName();
int userId = userMapper.getUserIdByEmail(email);
review.setUserId(userId);
model.addAttribute("review", review);
return "books/show";
}
...
}
```
위 코드가 정상 동작하기위해 ReviewMapper에 getReviews() 메소드를 추가해준다.
**ReviewMapper.java**
```java
public interface ReviewMapper {
@Insert("INSERT INTO reviews (text, book_id, user_id) VALUES (#{text}, #{bookId}, #{userId})")
void create(Review review);
@Select("SELECT * FROM reviews WHERE book_id = #{bookId} ORDER BY id DESC")
@Results(value = {
@Result(property = "id", column = "id"),
@Result(property = "text", column = "text"),
@Result(property = "bookId", column = "book_id"),
@Result(property = "userId", column = "user_id"),
@Result(property = "user", column = "id", javaType = User.class, one = @One(select = "getUserById"))
})
List<Review> getReviews(int bookId);
@Select("select * from users where id = #{userId}")
public User getUserById(int userId);
}
```
최종적으로 뷰페이지 통해 해당 리뷰들을 보여준다.
**views/books/show.jsp**
```jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="f"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>
<%@ page pageEncoding="utf-8"%>
<div class="jumbotron">
<h1>${ book.title }</h1>
<p>${ book.author }저</p>
</div>
<div class="container">
<div class="row">
<div class="col-6">
<img src="${ book.image }">
</div>
<div class="col-6">
<h3>${ book.title }</h3>
<p>저자: ${ book.author }</p>
</div>
</div>
<div class="my-5">
<h3>리뷰</h3>
</div>
<c:if test="${ fn:length(reviews) gt 0 }">
<table class="table table-stripped">
<thead>
<tr>
<th>User</th>
<th>Text</th>
</tr>
</thead>
<tbody>
<c:forEach var="review" items="${ reviews }">
<tr>
<td>${ review.user.email }</td>
<td>${ review.text }</td>
</tr>
</c:forEach>
</tbody>
</table>
</c:if>
<c:url var="reviewsPath" value="/reviews" />
<f:form modelAttribute="review" action="${ reviewsPath }" method="post">
<c:forEach var="error" items="${ fieldErrors }">
<div class="alert alert-warning">
<strong>${ error.getField() }</strong>: ${ error.getDefaultMessage() }
</div>
</c:forEach>
<f:textarea path="text" cssClass="form-control" rows="5" />
<f:hidden path="bookId" />
<f:hidden path="userId" />
<button class="btn btn-block btn-primary" type="submit">리뷰 등록</button>
</f:form>
</div>
```
### 최종 결과!!
![Imgur](http://i.imgur.com/hLLcSoY.png)
## 연습하기: 음식별 재료 등록
아래의 다이어그램을 참고하여 foods의 CRUD를 구현하고, 각 food 별 ingredients를 CRUD 하게 하시오.
![Imgur](https://i.imgur.com/MwJO9R2.png)
### 음식-재료 추가 설명
예를들어 에그마요 샌드위치를 클릭한 경우, 아래와 같은 정보가 표시되어야 한다.
+ 에그마요 샌드위치(기본가: 1000원)
- 식빵 (500원) ✅
- 계란 (500원) ✅
- 마요네즈 (500원) ✅
- 샐러드: 감자,당근,오이 (500원) ✅
- 오이 피클 (500원)
+ 최종 가격: 3000원