스프링 부트 타임리프 기본 설정
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
- resources: templates/ + {ViewName} + .html
- resources: templates/home.html
Model 문법
@GetMapping(value = "/members/new")
public String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
- Model: 스프링 MVC에서 컨트롤러와 뷰 사이의 데이터 전송을 담당하는 인터페이스
- model.addAttribute("memberForm", new MemberForm())
: MemberForm 객체 생성 - 모델에 "memberForm"이라는 이름으로 추가 - 모델에 추가된 데이터는 뷰에서 사용 가능 - 여기서 memberForm은 사용자 입력을 받기 위한 폼 데이터를 임시로 저장하는 객체
//createMemberForm.html
<form role="form" action="/members/new" th:object="${memberForm}" method="post">
- <form> 태그는 웹 페이지에서 사용자 입력을 수집하기 위해 사용
- submit(제출)시, 앞서 생성된 memberForm에 맞게 사용자가 입력한 데이터를 /members/new URLdmf Post 방식으로 요청
BindingResult
@PostMapping(value = "/members/new")
public String create(@Valid MemberForm form, BindingResult result) {
if (result.hasErrors()) {
return "members/createMemberForm";
}
- @Valid 주석을 통해 검증 -> 결과(오류 포함)는 BindingResult 객체에 저장
- 메소드 파라미터에서 @Valid로 검증된 객체 바로 뒤에 위치해야 합니다.
폼 객체를 사용하는 이유
- 엔티티에 화면을 위한 로직까지 전부 포함시키면 유지보수 hard
- 엔티티는 반드시 핵심 비즈니스 로직만 O
- 화면을 위한 로직은 X -> 폼 객체/DTO로 대체
상품 수정 페이지 API
@GetMapping("/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
Optional<Item> item = itemService.findOne(itemId);
if(item.isPresent()){
Book book = (Book) item.get();
BookForm form = new BookForm();
form.setId(book.getId());
form.setAuthor(book.getAuthor());
form.setIsbn(book.getIsbn());
form.setName(book.getName());
form.setStockQuantity(book.getStockQuantity());
form.setPrice(book.getPrice());
model.addAttribute("form", form);
return "items/updateItemForm";
}
return "redirect:/items";
}
- 우리가 일상적으로 사용하는 수정 페이지를 보면, 이전에 적었던 정보가 노출되어있다.
- 위 코드도 이전에 작성한 정보를 노출하고자 form(Bookform 객체)를 새로 만드록 model.addAttribute()하는 거임
엔티티 생명주기
1) 비영속(new/transient): 영속성 컨텍스트와 전혀 관계없는 새로운 상태
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
2) 영속(managed): 영속성 컨텍스트에 관리되는 상태 - 주로 entity 저장이나 조회를 통해
....
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction()
tx.begin();
em.persist(member);
3) 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
- em.detach(entity): 특정 엔티티만 준영속 상태로 전환
- em.clear(): 영속성 컨텍스트 완전 초기화 - 테스트 케이스 작성시 쿼리 보고싶을 때 활용
- em.close(): 영속성 컨텍스트 종료
4) 삭제(removed) - 실제 DB에서 지우겠어!
준영속 엔티티의 수정 방법
@PostMapping("/{itemId}/edit")
public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form){
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/items";
}
- 여기서 book은 준영속 엔티티(영속성 컨텍스트에 저장되었다가 분리된 상태)
- 준영속 엔티티를 수정하는 2가지 방법: 변경 감지 기능 사용/병합 -> BUT 엔티티 변경 시, 항상 변경 감지 사용
- 변경 감지 기능: 특정 속성(필드)만 변경 가능 / 식별자를 활용해 DB에 있는 엔티티를 가져온 다음 해당 필드만 변경
- 병합(merge): 모든 속성(필드) 변경 / 식별자를 활용해 DB에 있는 해당 엔티티를 통으로 수정 후 영속화
- 실무에서는 변경 감지 기능을 활용 -> 실무에서 모든 필드를 다 변경하게 두지 않기 때문 & 병합시 값 없으면 null로 업데이트 되버림
엔티티 변경할 때 유의점
- Controller 계층에서 어설프게 엔티티 생성X
- 트랜잭션이 있는 서비스 계층에서 식별자(id)와 변경할 데이터를 parmeter나 dto 형태로 명확하게 전달
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티 조회, 엔티티 데이터 직접 변경
- 트랜젝션 커밋 시점에 변경 감지가 실행
주문 조회(Repository 계층)
public List<Order> findAllByString(OrderSearch orderSearch) {
String jpql = "select o from Order o where o.member m";
Boolean isFirstCondition = true;
if(orderSearch.getOrderStatus() != null){
if(isFirstCondition){
jpql += "where ";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " o.status = :status";
}
if(StringUtils.hasText(orderSearch.getMemberName())){
if(isFirstCondition){
jpql += " where";
isFirstCondition = false;
} else {
jpql += " and";
}
jpql += " m.name like :name";
}
TypedQuery<Order> query = em.createQuery(jpql, Order.class)
.setMaxResults(1000);
if(orderSearch.getOrderStatus() != null) {
query.setParameter("status", orderSearch.getOrderStatus());
}
if(orderSearch.getMemberName() != null){
query.setParameter("name", orderSearch.getMemberName());
}
return query.getResultList();
}
- StringUtils.hasText(): 주어진 문자열(String)이 실제 텍스트 포함하는지 확인
-> 문자열이 null이 아니며, 길이가 0보다 크고, 하나 이상의 비공백 문자를 포함하고 있을 때 true 반환 - em.createQuery : 쿼리 만들기
- .setParameter() : parameter 바인딩하기
- .getResultList() : 해당 쿼리 날려서 결과 반환하기
header & footer 설정(feat.thymeleaf 기초 문법)
<head th:replace="fragments/header :: header">
...
<div th:replace="fragments/footer :: footer" />
- th:replace 속성: 지정된 템플릿의 일부를 현재 템플릿의 해당 부분으로 대체합니다.
- fragments/header: 대체할 템플릿 파일의 경로 지정
- :: header: 대체할 템플릿 파일 내에서 특정 이름의 템플릿 조각을 참조
상품 수정 페이지 HTML
<form th:object="${form}" method="post">
- action 속성이 생략되어 있을 때, 폼은 현재 페이지의 URL로 데이터를 제출
- GetMapping과 PostMapping의 URI:/{itemId}/edit 가 같음
Thymeleaf 문법
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요">
- <label> : 특정 폼 필드와 연관되어 있음을 나타냄
- th:for="name" : "name"이라는 필드와 연관되어있다
- th:field="*{name}" : 폼 필드와 객체의 속성(name)을 바인딩
회원 목록 뷰
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
<td th:text="${member.address?.city}"></td>
<td th:text="${member.address?.street}"></td>
<td th:text="${member.address?.zipcode}"></td>
</tr>
</tbody>
- <tr th:each="member : ${members}">: members 컬렉션(예: List, Set 등)에서 각 member 객체를 반복하여 표의 행 생성
- ?. 연산자(Safe Navigation Operator 또는 Elvis Operator) : address가 null인 경우 NullPointerException을 방지
Redirect
@PostMapping("/cancel/{orderId}")
public String cancel(@PathVariable("orderId") Long orderId){
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
- "redirect:/orders" 에서 /orders는 html 파일이 아니라 "/orders를 처리하는 다른 컨트롤러 메서드로 연결"
HomeController 관련 주의점
@Controller("/order")
@RequiredArgsConstructor
public class OrderController {
...
@GetMapping
public String createForm(Model model){
List<Member> memberList = memberService.findMemberList();
List<Item> itemList = itemService.findAll();
model.addAttribute("members", memberList);
model.addAttribute("items", itemList);
return "order/orderForm";
}
- 이렇게 작성하면 Spring이 homeController랑 헷갈려서 잘못 매핑할 수 있으므로 주의
- 위와 같은 경우는 맨 위 @Controller("/order")가 아니라, 각 API 마다 @GetMapping("/order")를 붙이는 게 좋음
'Dev > Spring Boot' 카테고리의 다른 글
| [실전! 스프링 데이터 JPA] ch 1 ~ ch 4요약정리 (0) | 2024.03.14 |
|---|
댓글