삭제,수정 버튼 존재하는 modify.html
<form id="postModifyForm"> <div> <label class="form-label" for="id">번호</label> <input class="form-control" id="id" name="id" th:value="${post.id}" readonly/> </div> <div> <label class="form-label" for="title">제목</label> <input class="form-control" id="title" name="title" th:value="${post.title}" /> </div> <div> <label class="form-label" for="content">내용</label> <textarea class="form-control" id="content" name="content" th:text="${post.content}" ></textarea> </div> <div> <label class="form-label" for="author">작성자</label> <input class="form-control" id="author" th:value="${post.author}" readonly/> </div> </form> <div> <!-- 버튼또는 input type=submit이 폼 안에 있는 경우: 무조건 submit -> get 방식(요청 주소가 ?id = {}&title ={}&content={}으로 변화) ==> form에서 요청 방식을 지정하지 않았기에 get방식으로 넘어감. + action주소도 지정안함. --> <button class="my-2 btn btn-outline-danger" id="btnDelete" >삭제</button> <button class="my-2 btn btn-outline-success" id="btnUpdate" >업데이트</button> </div>
* button 작동 java Script 코드
// 삭제 버튼을 찾음.
const btnDelete = document.querySelector('button#btnDelete');
btnDelete.addEventListener('click', (e) => {
const result = confirm(`정말 ${id}를 삭제할까요?`);
console.log(`결과 = ${result}`)
console.log(`id 값 = ${id}`)
if (!result) {
return;
}
// 절대 경로: '/post/delete'
// 맨 처음 '/' 의미:
// -> 클라이언트 포트번호 다음 주소
// -> 서버에서는 context-root를 의미.
form.action = '/post/delete'; // submit 요청 주소, 그전 주소: 'delete'도 가능함.
form.method = 'post' // submit 요청 방식.
form.submit(); // 폼 제출(submit), 요청 보내기.
});
* controller
// 삭제
@PostMapping("/delete")
public String delete(Long id) {
log.info("delete(id = {})", id);
// PostService를 이용해서 DB 테이블에서 포스트를 삭제하는 서비스 호출:
postService.delete(id);
return "redirect:/post";
}
* service
// id별 삭제
public void delete(Long id) {
log.info("delete(id = {})", id);
postRepository.deleteById(id);
}
* button 작동 java Script 코드
// 수정 버튼을 찾음.
const btnUpdate = document.querySelector('button#btnUpdate');
btnUpdate.addEventListener('click', (e) => {
// 만약 이벤트핸들러(콜백 함수) 외부에 있을 경우에는 html 문서가 완성된 상태에서 값이 들어감. 즉, 원래 있던 값, 미리 읽어둔 값이 존재.
//-> 변경 내용을 처리할 수 없음.
// 내부에서 변수 선언을 한 경우에는 버튼이 클릭된 당시에 값을 검사 및 읽음.
// 제목을 찾음.
const title = document.querySelector('#title').value;
// 내용을 찾음.
const content = document.querySelector('#content').value;
if (title === '' || content === '') {
alert('제목, 내용을 입력하세요');
return; // 함수 종료.
}
const result = confirm(`${id}를 수정할까요?`)
if (!result) {
return;
}
form.action = 'update';
form.method = 'post';
form.submit();
});
* controller
// 수정
// form의 name 속성과 DTO 필드 값이 같을 경우 찾을 수 있음.
@PostMapping("/update")
public String update(PostUpdateDto dto) {
log.info("update(dto={})", dto);
// 포스트 업데이트 서비스 호출:
/*
* Post entity = postRepository.findById(dto.getId()).orElseThrow();
* entity.update(dto);
*/
postService.update(dto);
// 쿼리 스트링에서는 중간에 공백이 있으면 안됨.
return "redirect:/post/details?id=" + dto.getId();
}
* service
// id별 수정
// Junit Test와는 다름.
// readOnly = false(기본값): select 과정이 늦을 수도 있음. 변경되고 있는지를 추적하고 관리하며, 변경시 DB와 연동이
// 됨.
// readOnly = true: 읽기만 하고, DB에 수정 변경 안됨.
@Transactional // (1)
public void update(PostUpdateDto dto) {
log.info("update(dto ={})", dto);
// (1) 메서드에 @Transactional 애너테이션을 설정하고,
// (2) DB에서 entity를 검색하고,
// (3) 검색한 엔터티를 수정하면,
// 틀랙잭션이 끝나는 시점에 DB update가 자동으로 수행됨!
// 실제로 존재하는 entity의 경우에는 오류를 날리지 않고 있다고 알림.
Post entity = postRepository.findById(dto.getId()).orElseThrow(); // (2)
entity.update(dto); // (3)
}
+ Junit Test
// @Test
// @Transactional
public void update() {
// 업데이트하기 전의 엔터티 검색:
// 검색은 리턴 결과가 존재 할수도 없을 수도 있기에 Post 타입이 아닌 Optional로 지정함.
// Post라는 타입을 꺼내주는 메서드: orElseThrow -값이 있으면 리턴 없으면 예외(오류)를 던져 버림.
Post entity = postRepository.findById(61L)
.orElseThrow();
log.info("update 전: {}", entity);
log.info(" update 전 수정 시간: {}", entity.getModifiedTime());
// entity를 변경할 내용을 가지고 있는 객체 생성:
PostUpdateDto dto = new PostUpdateDto();
dto.setTitle("JPA update 테스트");
dto.setContent("JPA Hibernate를 사용한 DB 테이블 업데이트");
// entity를 수정:
entity.update(dto);
// DB 테이블 업데이트:
// JPA에서는 insert와 update 메서드가 구분되어 있지 않음.
// save() 메서드의 argument가 DB에 없는 entity이면 insert, DB에 있는 entity이면 update를 실행.
// save: 커밋되는 순간이 시간 차이가 존재
// saveAndFlush: 변경 사항 즉각 반응됨. 바로 커밋.
postRepository.saveAndFlush(entity);
}
postSearchDto class
package com.itwill.spring4.dto;
import lombok.Data;
@Data
public class PostSearchDto {
// reqeustParameter와 동일한 이름 사용하기.
private String type;
private String keyword;
}
목록 read.html코드
<div class="card-footer">
<!-- 검색은 보통 get 방식 -->
<form method="get" th:action="@{/post/search}">
<div class="row">
<div class="col-3">
<select class="form-select" name="type">
<option value="t">제목</option>
<option value="c">내용</option>
<option value="tc">제목+내용</option>
<option value="a">작성자</option>
</select>
</div>
<div class="col-8">
<input class="form-control" name="keyword" type="text" placeholder="검색어 입력" required/>
</div>
<div class="col-1">
<input class="form-control btn btn-outline-dark" type="submit" value="검색" />
</div>
</div>
</form>
</div>
controller
@GetMapping("/search")
public String search(PostSearchDto dto, Model model) {
log.info("search(dto = {})",dto);
// postService의 검색 기능 호출:
List<Post> list = postService.search(dto);
// 검색 결과를 Model에 저장해서 뷰에 전달:
model.addAttribute("posts", list);
return "/post/read";
}
Service
@Transactional(readOnly = true)
// 읽기만 하고 수정을 안 하기에 즉, 읽기 전용 용도이기에 Select 속도를 빠르게 하기 위해서 애너테이션 설정.
public List<Post> search(PostSearchDto dto) {
log.info("search(dto ={})", dto);
List<Post> list = null;
switch (dto.getType()) {
case "t":
list = postRepository.findByTitleContainsIgnoreCaseOrderByIdDesc(dto.getKeyword());
break;
case "c":
list = postRepository.findByContentContainsIgnoreCaseOrderByIdDesc(dto.getKeyword());
break;
case "tc":
list = postRepository.searchByKeyword(dto.getKeyword());
break;
case "a":
list = postRepository.findByAuthorContainsIgnoreCaseOrderByIdDesc(dto.getKeyword());
break;
}
return list;
}
Repository
package com.itwill.spring4.repository.post;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
// crud 작업을 할 entity class이름 작성, 그 class의 id 컬럼의 타입.
public interface PostRepository extends JpaRepository<Post, Long> {
// 제목으로 검색:
/*
* ?: prepared Statement
*
* SQL 문장
* select * from posts P
* where lower(p.title) like lower('%' || ? || '%')
* order by p.id desc;
*
* ||: 붙여주는 역할
* lower, upper 등 상관없이 사용 가능함.
*/
// JPA에서
// Contains: 키워드를 포함하고 있으면 -> Like 검색을 만들어주는 키워드
// IgnoreCase: 대소문자 구분없이
// where == By~: 뒤에 있는 글은 Post의 필드 값(컬럼 이름)이어야 하며, 만약 값이 Title일 경우 파람도 같은 걸로 해야 함.
List<Post> findByTitleContainsIgnoreCaseOrderByIdDesc(String title);
// 내용으로 검색:
/*
* ?: prepared Statement
*
* SQL 문장
* select * from posts P
* where lower(p.content) like lower('%' || ? || '%')
* order by p.id desc;
*/
List<Post> findByContentContainsIgnoreCaseOrderByIdDesc(String Content);
// 작성자로 검색:
/*
* ?: prepared Statement
*
* SQL 문장
* select * from posts P
* where lower(p.author) like lower('%' || ? || '%')
* order by p.id desc;
*/
List<Post> findByAuthorContainsIgnoreCaseOrderByIdDesc(String author);
// 제목 또는 내용으로 검색:
/*
* select * from posts p
* where lower(p.title) like lower('%' || ? || '%')
* or lower(p.content) like lower('%' || ? || '%')
* order by p.id desc;
*/
List<Post> findByTitleContainsIgnoreCaseOrContentContainsIgnoreCaseOrderByIdDesc(String title, String Content);
// JPQL(JPA Query Language) 문법으로 쿼리를 작성하고, 그 쿼리를 실행하는 메서드 이름을 설정
// JPQL은 Entity 클래스의 이름과 필드 이름들을 사용해서 작성.
// (주의) DB 테이블 이름과 컬럼 이름을 사용하지 않음!
/*
* :keyword -> 변수 이름
* 변수이름이 서로 다를 경우 다르게 사용해도 가능함.
* 변수 이름을 동일하게 사용시 param이 한 개로 채워지게 됨.
*
* 모든 컬럼: entity 클래스에 별명 준 것사용, P/ *은 안됨.
* 일부 컬럼: p.id, p.title,...
*/
// @Param("keyword") 값에 argument keyword를 넘긴다.
@Query(
"select p from Post p " +
"where lower(p.title) like lower('%' || :keyword || '%') " +
"or lower(p.content) like lower('%' || :keyword || '%') " +
"order by p.id desc"
)
List<Post> searchByKeyword(@Param("keyword") String keyword);
}