๋ฐ์ดํฐ๊ฐ ๊ณ์ ์์ฌ๊ฐ๋ ๊ฒ์ํ์ด๋ฏ๋ก ๊ฒ์๊ธฐ๋ฅ์ ํ์๋ผ๊ณ ํ ์ ์๋ค.
๊ฒ์ํ ๋ด์ฉ์ด ์ง๋ฌธ์ ์ ๋ชฉ, ์ง๋ฌธ์ ๋ด์ฉ, ์ง๋ฌธ ์์ฑ์, ๋ต๋ณ์ ๋ด์ฉ, ๋ต๋ณ ์์ฑ์์ ์กด์ฌํ๋์ง ์ฐพ์๋ณด๊ณ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ํ๋ฉด์ ์ถ๋ ฅํด๋ณด์!
select
distinct q.id,
q.author_id,
q.content,
q.create_date,
q.modify_date,
q.subject
from question q
left outer join site_user u1 on q.author_id=u1.id
left outer join answer a on q.id=a.question_id
left outer join site_user u2 on a.author_id=u2.id
where
q.subject like '%์คํ๋ง%'
or q.content like '%์คํ๋ง%'
or u1.username like '%์คํ๋ง%'
or a.content like '%์คํ๋ง%'
or u2.username like '%์คํ๋ง%'
์์ ์ฟผ๋ฆฌ์์ ๋ณธ ๊ฒ๊ณผ ๊ฐ์ด ์ฌ๋ฌ ํ ์ด๋ธ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ํด์ผ ํ ๊ฒฝ์ฐ์๋ JPA๊ฐ ์ ๊ณตํ๋ Specification ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ํธ๋ฆฌํ๋ค.
์ฐธ๊ณ : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications
import com.mysite.sbb.answer.Answer;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import org.springframework.data.jpa.domain.Specification;
private Specification<Question> search(String kw) {
return new Specification<>() {
private static final long serialVersionUID = 1L;
@Override
// Root<Question> q : ๊ธฐ์ค์ ์๋ฏธํ๋ Question ์ํฐํฐ์ ๊ฐ์ฒด
public Predicate toPredicate(Root<Question> q, CriteriaQuery<?> query, CriteriaBuilder cb) {
query.distinct(true); // ์ค๋ณต์ ์ ๊ฑฐ
/*
* from question q
* left outer join site_user u1 on q.author_id = u1.id
*/
// Question, SiteUser ์ํฐํฐ๋ author ์์ฑ์ผ๋ก ์ฐ๊ฒฐ๋์ด ์๊ธฐ ๋๋ฌธ์ q.join("author")์ ๊ฐ์ด ์กฐ์ธ
Join<Question, SiteUser> u1 = q.join("author", JoinType.LEFT);
/*
* from question q
* left outer join answer a on q.id=a.question_id
*/
// Question๊ณผ Answer ์ํฐํฐ๋ "answerList"๋ก ์ฐ๊ฒฐ๋์ด์๋ค.
Join<Question, Answer> a = q.join("answerList", JoinType.LEFT);
/*
* left outer join site_user u2 on a.author_id = u2.id
*/
// Answer, SiteUser ์ํฐํฐ๋ "author"๋ก ์ฐ๊ฒฐ๋์ด ์๋ค.
Join<Answer, SiteUser> u2 = a.join("author", JoinType.LEFT);
// ์
๋ ฅ๋ฐ์ ๋ฌธ์ "kw"๋ฅผ ํฌํจ๋์ด ์๋ ๊ฒ์ ๋ฆฌํด
return cb.or(cb.like(q.get("subject"), "%" + kw + "%"), // ์ง๋ฌธ ์ ๋ชฉ
cb.like(q.get("content"), "%" + kw + "%"), // ์ง๋ฌธ ๋ด์ฉ
cb.like(u1.get("username"), "%" + kw + "%"), // ์ง๋ฌธ ์์ฑ์
cb.like(a.get("content"), "%" + kw + "%"), // ๋ต๋ณ ๋ด์ฉ
cb.like(u2.get("username"), "%" + kw + "%")); // ๋ต๋ณ ์์ฑ์
}
};
}
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
public interface QuestionRepository extends JpaRepository<Question, Integer> {
Question findBySubject(String subject);
Question findBySubjectAndContent(String subject, String content);
List<Question> findBySubjectLike(String subject);
Page<Question> findAll(Pageable pageable);
Page<Question> findAll(Specification<Question> spec, Pageable pageable);
}
โ
์ถ๊ฐ : Page<Question> findAll(Specification<Question> spec, Pageable pageable);
// ๊ฒ์์ด๋ฅผ ์๋ฏธํ๋ kw ๋งค๊ฐ๋ณ์์ ์ถ๊ฐ
public Page<Question> getList(int page, String kw){
List<Sort.Order> sorts = new ArrayList<>();
sorts.add(Sort.Order.desc("createDate"));
Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
Specification<Question> spec = search(kw); // ๊ฐ์ฒด๋ฅผ ์์ฑ
return this.questionRepository.findAll(spec, pageable); // ๊ฒ์๊ฒฐ๊ณผ ๋ฆฌํด
}
@GetMapping("/list")
// ๊ฒ์์ด์ ํด๋นํ๋ kw ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ๊ณ ๋ํดํธ๊ฐ์ผ๋ก ๋น ๋ฌธ์์ด์ ์ค์
public String list(Model model, @RequestParam(value="page", defaultValue="0") int page, @RequestParam(value="kw", defaultValue="") String kw) {
Page<Question> paging = this.questionService.getList(page, kw);
model.addAttribute("paging", paging);
// ํ๋ฉด์์ ์
๋ ฅํ ๊ฒ์์ด๋ฅผ ํ๋ฉด์ ์ ์งํ๊ธฐ ์ํด model.addAttribute("kw", kw)๋ก kw ๊ฐ์ ์ ์ฅ
model.addAttribute("kw", kw);
return "question_list";
}
โ ์ํ๋ ์์น์ ๊ฒ์์ฐฝ์ ์ถ๊ฐํด๋ณด์!
<div class="row my-3">
<div class="col-6">
<div class="input-group">
<input type="text" id="search_kw" class="form-control" th:value="${kw}">
<button class="btn btn-outline-secondary" type="button" id="btn_search">์ฐพ๊ธฐ</button>
</div>
</div>
</div>
โ
<input type="text" id="search_kw" class="form-control" th:value="${kw}">
์๋ฐ ์คํฌ๋ฆฝํธ์์ ์ด ํ ์คํธ์ฐฝ์ ์ ๋ ฅ๋ ๊ฐ์ ์ฝ๊ธฐ ์ํด ๋ค์์ฒ๋ผ ํ ์คํธ์ฐฝ id ์์ฑ์ "search_kw"๋ผ๋ ๊ฐ์ ์ถ๊ฐํ์๋ค.
id ์์ฑ์ ์๋ฐ์คํฌ๋ฆฝํธ์์ ์ฝ์ ์ ์๋ค.
โ page์ kw๋ฅผ ๋์์ GET์ผ๋ก ์์ฒญํ ์ ์๋ searchForm ์ถ๊ฐ
(... ์๋ต ...)
<!-- ํ์ด์ง์ฒ๋ฆฌ ๋ -->
<form th:action="@{/question/list}" method="get" id="searchForm">
<input type="hidden" id="kw" name="kw" th:value="${kw}">
<input type="hidden" id="page" name="page" th:value="${paging.number}">
</form>
</div>
</html>
GET ๋ฐฉ์์ผ๋ก ์์ฒญํด์ผ ํ๋ฏ๋ก method ์์ฑ์ "get"์ ์ค์ ํ๋ค. kw์ page๋ ์ด์ ์ ์์ฒญํ๋ ๊ฐ์ ๊ธฐ์ตํ๊ณ ์์ด์ผ ํ๋ฏ๋ก value์ ๊ฐ์ ์ ์งํ ์ ์๋๋ก ํ๋ค.
๐ก GET ๋ฐฉ์์ ์ฌ์ฉํ๋ ์ด์
๋ง์ฝ GET์ด ์๋ POST ๋ฐฉ์์ผ๋ก ๊ฒ์๊ณผ ํ์ด์ง์ ์ฒ๋ฆฌํ๋ค๋ฉด ์น ๋ธ๋ผ์ฐ์ ์์ "์๋ก๊ณ ์นจ" ๋๋ "๋ค๋ก๊ฐ๊ธฐ"๋ฅผ ํ์ ๋ "๋ง๋ฃ๋ ํ์ด์ง์ ๋๋ค."๋ผ๋ ์ค๋ฅ๋ฅผ ์ข ์ข ๋ง๋๊ฒ ๋ ๊ฒ์ด๋ค. ์๋ํ๋ฉด POST ๋ฐฉ์์ ๋์ผํ POST ์์ฒญ์ด ๋ฐ์ํ ๊ฒฝ์ฐ ์ค๋ณต ์์ฒญ์ ๋ฐฉ์งํ๊ธฐ ์ํด "๋ง๋ฃ๋ ํ์ด์ง์ ๋๋ค." ๋ผ๋ ์ค๋ฅ๋ฅผ ๋ฐ์์ํค๊ธฐ ๋๋ฌธ์ ์ฌ๋ฌ ํ๋ผ๋ฏธํฐ๋ฅผ ์กฐํฉํ์ฌ ๊ฒ์๋ฌผ ๋ชฉ๋ก์ ์กฐํํ ๋๋ GET ๋ฐฉ์์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋ค.
why? ๊ฒ์์ด๊ฐ ์์ ๊ฒฝ์ฐ ๊ฒ์์ด์ ํ์ด์ง ๋ฒํธ๋ฅผ ํจ๊ป ์ ์กํด์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
โ
์์ ์ : th:href="@{|?page=${paging.number-1}|}"
โ
์์ ํ : href="javascript:void(0)" th:data-page="${paging.number-1}"
<script layout:fragment="script" type='text/javascript'>
const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
element.addEventListener('click', function() {
document.getElementById('page').value = this.dataset.page;
document.getElementById('searchForm').submit();
});
});
const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
document.getElementById('kw').value = document.getElementById('search_kw').value;
document.getElementById('page').value = 0; // ๊ฒ์๋ฒํผ์ ํด๋ฆญํ ๊ฒฝ์ฐ 0ํ์ด์ง๋ถํฐ ์กฐํํ๋ค.
document.getElementById('searchForm').submit();
});
</script>
const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
element.addEventListener('click', function() {
document.getElementById('page').value = this.dataset.page;
document.getElementById('searchForm').submit();
});
});
โ ์ด class ์์ฑ๊ฐ์ผ๋ก "page-link"๋ผ๋ ๊ฐ์ ๊ฐ์ง๊ณ ์๋ ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด
์ด ๋งํฌ์ data-page ์์ฑ๊ฐ์ ์ฝ์ด searchForm์ page ํ๋์ ์ค์ ํ์ฌ searchForm์ ์์ฒญํ๋ค.
const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
document.getElementById('kw').value = document.getElementById('search_kw').value;
document.getElementById('page').value = 0; // ๊ฒ์๋ฒํผ์ ํด๋ฆญํ ๊ฒฝ์ฐ 0ํ์ด์ง๋ถํฐ ์กฐํํ๋ค.
document.getElementById('searchForm').submit();
});
โ ๊ฒ์๋ฒํผ์ ํด๋ฆญํ๋ฉด ๊ฒ์์ด ํ ์คํธ์ฐฝ์ ์ ๋ ฅ๋ ๊ฐ์ searchForm์ kw ํ๋์ ์ค์ ํ์ฌ searchForm์ ์์ฒญํ๋๋ก ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํ๋ค. ๊ฒ์๋ฒํผ์ ํด๋ฆญํ๋ ๊ฒฝ์ฐ๋ ์๋ก์ด ๊ฒ์์ ํด๋น๋๋ฏ๋ก page์ ํญ์ 0์ ์ค์ ํ์ฌ ์ฒซ ํ์ด์ง๋ก ์์ฒญํ๋๋ก ํ๋ค.
๐ก Specification ๋์ ์ง์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์ฌ ์ํํ๋ ๋ฐฉ๋ฒ
๐พ QuestionRepository์ ๋ฉ์๋ ์ถ๊ฐ
โ @Query ์ ๋ํ ์ด์ ์ด ์ ์ฉ๋ findAllByKeyword ๋ฉ์๋๋ฅผ ์ถ๊ฐ
import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param;
@Query("select " + "distinct q " + "from Question q " + "left outer join SiteUser u1 on q.author=u1 " + "left outer join Answer a on a.question=q " + "left outer join SiteUser u2 on a.author=u2 " + "where " + " q.subject like %:kw% " + " or q.content like %:kw% " + " or u1.username like %:kw% " + " or a.content like %:kw% " + " or u2.username like %:kw% ") Page<Question> findAllByKeyword(@Param("kw") String kw, Pageable pageable);
โ QuestionService์์ Page getList ๋ฉ์๋ ์์ ํ๊ธฐ
โ ์์ ์ : return this.questionRepository.findAll(spec, pageable); โ ์์ ํ : return this.questionRepository.findAllByKeyword(kw, pageable);
@Query๋ฅผ ์์ฑํ ๋์๋ ๋ฐ๋์ ํ ์ด๋ธ ๊ธฐ์ค์ด ์๋ ์ํฐํฐ ๊ธฐ์ค์ผ๋ก ์์ฑํด์ผ ํ๋ค. ์ฆ, site_user์ ๊ฐ์ ํ ์ด๋ธ๋ช ๋์ SiteUser์ฒ๋ผ ์ํฐํฐ๋ช ์ ์ฌ์ฉํด์ผ ํ๊ณ ์กฐ์ธ๋ฌธ์์ ๋ณด๋ฏ์ด q.author_id=u1.id์ ๊ฐ์ ์ปฌ๋ผ๋ช ๋์ q.author=u1์ฒ๋ผ ์ํฐํฐ์ ์์ฑ๋ช ์ ์ฌ์ฉํด์ผ ํ๋ค.
๊ทธ๋ฆฌ๊ณ @Query์ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ kw ๋ฌธ์์ด์ ๋ฉ์๋์ ๋งค๊ฐ๋ณ์์ @Param("kw")์ฒ๋ผ @Param ์ ๋ํ ์ด์ ์ ์ฌ์ฉํด์ผ ํ๋ค. ๊ฒ์์ด๋ฅผ ์๋ฏธํ๋ kw ๋ฌธ์์ด์ @Query ์์์ :kw๋ก ์ฐธ์กฐ๋๋ค.
โ
๋ฐ์ดํฐ ์กฐํ์์ ๋ง์ด ์ฌ์ฉ๋๋ Specification ์ธํฐํ์ด์ค
โ
๋ฐ์ดํฐ ์กฐํ์ POST๋ฐฉ์์ด ์๋ GET๋ฐฉ์์ ์ฌ์ฉํ๋ ์ด์
โ
Specification ์ธํฐํ์ด์ค / JPA๊ณต๋ถํ๊ธฐ
โ
์๋ฐ์คํฌ๋ฆฝํธ ๊ณต๋ถ