Skip to content

Commit

Permalink
Merge pull request #530 from woowacourse-teams/refactor/495-find-all
Browse files Browse the repository at this point in the history
템플릿 조회에서 동적 쿼리 생성을 위한 Specification 구현
  • Loading branch information
zangsu authored Aug 22, 2024
2 parents d0e004d + 22562ad commit f133826
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

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;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.http.HttpStatus;
Expand All @@ -13,7 +15,8 @@
import codezap.template.domain.Template;

@SuppressWarnings("unused")
public interface TemplateJpaRepository extends TemplateRepository, JpaRepository<Template, Long> {
public interface TemplateJpaRepository extends TemplateRepository, JpaRepository<Template, Long>,
JpaSpecificationExecutor<Template> {

default Template fetchById(Long id) {
return findById(id).orElseThrow(
Expand All @@ -22,6 +25,8 @@ default Template fetchById(Long id) {

List<Template> findByMemberId(Long id);

Page<Template> findAll(Specification<Template> specification, Pageable pageable);

@Query("""
SELECT DISTINCT t
FROM Template t JOIN SourceCode s ON t.id = s.template.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;

import codezap.template.domain.Template;

Expand All @@ -15,6 +16,8 @@ public interface TemplateRepository {

List<Template> findByMemberId(Long id);

Page<Template> findAll(Specification<Template> specification, Pageable pageable);

Page<Template> searchBy(Long memberId, String keyword, Pageable pageable);

Page<Template> searchBy(Long memberId, String keyword, List<Long> templateIds, Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package codezap.template.repository;

import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery;

import org.springframework.data.jpa.domain.Specification;

import codezap.template.domain.Template;
import codezap.template.domain.TemplateTag;

public class TemplateSpecification implements Specification<Template> {
private final Long memberId;
private final String keyword;
private final Long categoryId;
private final List<Long> tagIds;

private TemplateSpecification(Long memberId, String keyword, Long categoryId, List<Long> tagIds) {
this.memberId = memberId;
this.keyword = keyword;
this.categoryId = categoryId;
this.tagIds = tagIds;
}

public static Specification<Template> withDynamicQuery(
Long memberId, String keyword, Long categoryId, List<Long> tagIds
) {
return new TemplateSpecification(memberId, keyword, categoryId, tagIds);
}

@Override
public Predicate toPredicate(Root<Template> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> predicates = new ArrayList<>();

addMemberPredicate(predicates, criteriaBuilder, root);
addCategoryPredicate(predicates, criteriaBuilder, root);
addTagPredicate(predicates, criteriaBuilder, root, query);
addKeywordPredicate(predicates, criteriaBuilder, root);

return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}

private void addMemberPredicate(List<Predicate> predicates, CriteriaBuilder criteriaBuilder, Root<Template> root) {
if (memberId != null) {
predicates.add(criteriaBuilder.equal(root.get("member").get("id"), memberId));
}
}

private void addKeywordPredicate(List<Predicate> predicates, CriteriaBuilder criteriaBuilder, Root<Template> root) {
if (keyword != null && !keyword.trim().isEmpty()) {
String likeKeyword = "%" + keyword.trim() + "%";
predicates.add(criteriaBuilder.or(criteriaBuilder.like(root.get("title"), likeKeyword),
criteriaBuilder.like(root.get("description"), likeKeyword)));
}
}

private void addCategoryPredicate(List<Predicate> predicates, CriteriaBuilder criteriaBuilder, Root<Template> root
) {
if (categoryId != null) {
predicates.add(criteriaBuilder.equal(root.get("category").get("id"), categoryId));
}
}

private void addTagPredicate(
List<Predicate> predicates, CriteriaBuilder criteriaBuilder, Root<Template> root, CriteriaQuery<?> query
) {
if (tagIds == null || tagIds.isEmpty()) {
return;
}
Subquery<Long> subquery = query.subquery(Long.class);
Root<TemplateTag> subRoot = subquery.from(TemplateTag.class);
subquery.select(subRoot.get("template").get("id")).where(subRoot.get("tag").get("id").in(tagIds))
.groupBy(subRoot.get("template").get("id"))
.having(criteriaBuilder.equal(criteriaBuilder.countDistinct(subRoot.get("tag").get("id")),
tagIds.size()));

predicates.add(root.get("id").in(subquery));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpStatus;

import codezap.global.exception.CodeZapException;
Expand Down Expand Up @@ -37,7 +38,6 @@ public boolean existsByCategoryId(Long categoryId) {
return templates.stream().anyMatch(template -> Objects.equals(template.getCategory().getId(), categoryId));
}

@Override
public Page<Template> searchBy(Long memberId, String keyword, Pageable pageable) {
List<Template> searchedTemplates = templates.stream()
.filter(template -> Objects.equals(template.getMember().getId(), memberId))
Expand All @@ -47,7 +47,6 @@ public Page<Template> searchBy(Long memberId, String keyword, Pageable pageable)
return pageTemplates(pageable, searchedTemplates);
}

@Override
public Page<Template> searchBy(Long memberId, String keyword, List<Long> templateIds, Pageable pageable) {
List<Template> searchedTemplates = templates.stream()
.filter(template -> Objects.equals(template.getMember().getId(), memberId))
Expand All @@ -58,7 +57,6 @@ public Page<Template> searchBy(Long memberId, String keyword, List<Long> templat
return pageTemplates(pageable, searchedTemplates);
}

@Override
public Page<Template> searchBy(Long memberId, String keyword, Long categoryId, Pageable pageable) {
List<Template> searchedTemplates = templates.stream()
.filter(template -> Objects.equals(template.getMember().getId(), memberId))
Expand All @@ -69,7 +67,6 @@ public Page<Template> searchBy(Long memberId, String keyword, Long categoryId, P
return pageTemplates(pageable, searchedTemplates);
}

@Override
public Page<Template> searchBy(Long memberId, String keyword, Long categoryId, List<Long> templateIds,
Pageable pageable
) {
Expand Down Expand Up @@ -113,6 +110,11 @@ public List<Template> findByMemberId(Long id) {
.toList();
}

@Override
public Page<Template> findAll(Specification<Template> specification, Pageable pageable) {
return null;
}

@Override
public Template save(Template entity) {
var saved = new Template(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package codezap.template.repository;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.domain.Specification;

import codezap.category.domain.Category;
import codezap.category.repository.CategoryRepository;
import codezap.global.repository.JpaRepositoryTest;
import codezap.member.domain.Member;
import codezap.member.repository.MemberRepository;
import codezap.tag.domain.Tag;
import codezap.tag.repository.TagRepository;
import codezap.tag.repository.TemplateTagRepository;
import codezap.template.domain.Template;
import codezap.template.domain.TemplateTag;

@JpaRepositoryTest
class TemplateRepositoryFindAllTest {

@Autowired
private TemplateRepository templateRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
private CategoryRepository categoryRepository;
@Autowired
private TagRepository tagRepository;
@Autowired
private TemplateTagRepository templateTagRepository;

private Member member1, member2;
private Category category1, category2;
private Tag tag1, tag2;

@BeforeEach
void setUp() {
member1 = memberRepository.save(new Member("[email protected]", "pp"));
member2 = memberRepository.save(new Member("[email protected]", "pp"));

category1 = categoryRepository.save(new Category("Category 1", member1));
category2 = categoryRepository.save(new Category("Category 2", member1));

tag1 = tagRepository.save(new Tag("Tag 1"));
tag2 = tagRepository.save(new Tag("Tag 2"));

Template template1 = new Template(member1, "Template 1", "Description 1", category1);
TemplateTag templateTag11 = new TemplateTag(template1, tag1);
TemplateTag templateTag12 = new TemplateTag(template1, tag2);
templateRepository.save(template1);
templateTagRepository.save(templateTag11);
templateTagRepository.save(templateTag12);

Template template2 = new Template(member1, "Template 2", "Description 2", category2);
TemplateTag templateTag21 = new TemplateTag(template2, tag1);
TemplateTag templateTag22 = new TemplateTag(template2, tag2);
templateRepository.save(template2);
templateTagRepository.save(templateTag21);
templateTagRepository.save(templateTag22);

Template template3 = new Template(member2, "Another Template", "Another Description", category1);
TemplateTag templateTag31 = new TemplateTag(template3, tag2);
templateRepository.save(template3);
templateTagRepository.save(templateTag31);
}

@Test
@DisplayName("회원 ID로 템플릿 조회")
void testFindByMemberId() {
Specification<Template> spec = TemplateSpecification.withDynamicQuery(member1.getId(), null, null, null);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(2);
assertThat(result.getContent()).allMatch(template -> template.getMember().getId().equals(member1.getId()));
}

@Test
@DisplayName("키워드로 템플릿 조회")
void testFindByKeyword() {
Specification<Template> spec = TemplateSpecification.withDynamicQuery(null, "Template", null, null);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(3);
assertThat(result.getContent()).allMatch(template ->
template.getTitle().contains("Template") || template.getDescription().contains("Template"));
}

@Test
@DisplayName("카테고리 ID로 템플릿 조회")
void testFindByCategoryId() {
Specification<Template> spec = TemplateSpecification.withDynamicQuery(null, null, category1.getId(),
null);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(2);
assertThat(result.getContent()).allMatch(template -> template.getCategory().getId().equals(category1.getId()));
}

@Test
@DisplayName("태그 ID 목록으로 템플릿 조회: 모든 태그를 가진 템플릿만 조회")
void testFindByTagIds() {
List<Long> tagIds = Arrays.asList(tag1.getId(), tag2.getId());

Specification<Template> spec = TemplateSpecification.withDynamicQuery(null, null, null, tagIds);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(2);
assertThat(result.getContent()).containsExactlyInAnyOrder(templateRepository.fetchById(1L),
templateRepository.fetchById(2L));
}

@Test
@DisplayName("단일 태그 ID로 템플릿 조회")
void testFindBySingleTagId() {
List<Long> tagIds = Arrays.asList(tag2.getId());

Specification<Template> spec = TemplateSpecification.withDynamicQuery(null, null, null, tagIds);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(3);
assertThat(result.getContent()).containsExactlyInAnyOrder(templateRepository.fetchById(1L),
templateRepository.fetchById(2L), templateRepository.fetchById(3L));
}

@Test
@DisplayName("회원 ID와 키워드로 템플릿 조회")
void testFindByMemberIdAndKeyword() {
Specification<Template> spec = TemplateSpecification.withDynamicQuery(member1.getId(), "Template", null,
null);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(2);
assertThat(result.getContent()).allMatch(template ->
template.getMember().getId().equals(member1.getId()) &&
(template.getTitle().contains("Template") || template.getDescription().contains("Template"))
);
}

@Test
@DisplayName("회원 ID와 카테고리 ID로 템플릿 조회")
void testFindByMemberIdAndCategoryId() {
Specification<Template> spec = TemplateSpecification.withDynamicQuery(member1.getId(), null, category1.getId(),
null);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(1);
assertThat(result.getContent().get(0).getMember().getId()).isEqualTo(member1.getId());
assertThat(result.getContent().get(0).getCategory().getId()).isEqualTo(category1.getId());
}

@Test
@DisplayName("회원 ID와 태그 ID 목록으로 템플릿 조회")
void testFindByMemberIdAndTagIds() {
List<Long> tagIds = Arrays.asList(tag1.getId(), tag2.getId());
Specification<Template> spec = TemplateSpecification.withDynamicQuery(member1.getId(), null, null,
tagIds);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(2);
assertThat(result.getContent()).containsExactlyInAnyOrder(templateRepository.fetchById(1L),
templateRepository.fetchById(2L));
}

@Test
@DisplayName("모든 검색 기준으로 템플릿 조회")
void testFindWithAllCriteria() {
List<Long> tagIds = Arrays.asList(tag1.getId(), tag2.getId());
Specification<Template> spec = TemplateSpecification.withDynamicQuery(member1.getId(), "Template",
category1.getId(), tagIds);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).hasSize(1);
assertThat(result.getContent()).containsExactlyInAnyOrder(templateRepository.fetchById(1L));
}

@Test
@DisplayName("검색 결과가 없는 경우 테스트")
void testFindWithNoResults() {
Specification<Template> spec = TemplateSpecification.withDynamicQuery(member1.getId(), "NonexistentKeyword",
null, null);
Page<Template> result = templateRepository.findAll(spec, PageRequest.of(0, 10));

assertThat(result.getContent()).isEmpty();
}
}

0 comments on commit f133826

Please sign in to comment.