Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

카페 검색 기능 개발 #443

Merged
merged 19 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ dependencies {
//Flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

//QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

def generatedQueryDsl = 'build/generated/querydsl'

sourceSets {
main.java.srcDirs += [generatedQueryDsl]
}

tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generatedQueryDsl))
}

clean.doLast {
file(generatedQueryDsl).deleteDir()
}

ext {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.project.yozmcafe.config;

import com.querydsl.jpa.impl.JPAQueryFactory;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.project.yozmcafe.controller.dto.cafe.CafeRankResponse;
import com.project.yozmcafe.controller.dto.cafe.CafeResponse;
import com.project.yozmcafe.controller.dto.cafe.CafeSearchRequest;
import com.project.yozmcafe.controller.dto.cafe.CafeSearchResponse;
import com.project.yozmcafe.domain.member.Member;
import com.project.yozmcafe.service.CafeService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
Expand Down Expand Up @@ -34,7 +37,7 @@ public ResponseEntity<List<CafeResponse>> getCafesForLoggedInMember(@LoginUser f

@GetMapping("/guest")
public ResponseEntity<List<CafeResponse>> getCafesForUnLoggedInMember(@PageableDefault(size = PAGE_SIZE) final Pageable pageable) {
List<CafeResponse> cafeResponses = cafeService.getCafesForUnLoginMember(pageable);
final List<CafeResponse> cafeResponses = cafeService.getCafesForUnLoginMember(pageable);
return ResponseEntity.ok(cafeResponses);
}

Expand All @@ -46,9 +49,15 @@ public ResponseEntity<List<CafeRankResponse>> getCafesOrderByLikeCount(@Pageable

@GetMapping("/{cafeId}")
public ResponseEntity<CafeResponse> getCafeById(@PathVariable("cafeId") final long cafeId) {
CafeResponse cafeResponse = cafeService.getCafeById(cafeId);
final CafeResponse cafeResponse = cafeService.getCafeById(cafeId);
return ResponseEntity.ok(cafeResponse);
}

@GetMapping("/search")
public ResponseEntity<List<CafeSearchResponse>> getCafeBySearch(final CafeSearchRequest cafeSearchRequest) {
final List<CafeSearchResponse> cafeSearchResponses = cafeService.getCafesBySearch(cafeSearchRequest);
return ResponseEntity.ok(cafeSearchResponses);
}
}


Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.project.yozmcafe.controller;

import com.project.yozmcafe.controller.dto.cafe.LikedCafeResponse;
import com.project.yozmcafe.controller.dto.cafe.LikedCafeThumbnailResponse;
import com.project.yozmcafe.controller.dto.cafe.CafeThumbnailResponse;
import com.project.yozmcafe.service.LikedCafeService;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
Expand All @@ -26,9 +26,9 @@ public LikedCafeController(final LikedCafeService likedCafeService) {
}

@GetMapping("/members/{memberId}/liked-cafes")
public ResponseEntity<List<LikedCafeThumbnailResponse>> getLikedCafeThumbnails(@PathVariable("memberId") final String memberId,
@PageableDefault(size = PAGE_SIZE) final Pageable pageable) {
final List<LikedCafeThumbnailResponse> likedCafes = likedCafeService.findLikedCafeThumbnailsByMemberId(memberId, pageable);
public ResponseEntity<List<CafeThumbnailResponse>> getLikedCafeThumbnails(@PathVariable("memberId") final String memberId,
@PageableDefault(size = PAGE_SIZE) final Pageable pageable) {
final List<CafeThumbnailResponse> likedCafes = likedCafeService.findLikedCafeThumbnailsByMemberId(memberId, pageable);

return ResponseEntity.ok(likedCafes);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.project.yozmcafe.controller.dto.cafe;

public record CafeSearchRequest(String cafeName, String menu, String address) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.project.yozmcafe.controller.dto.cafe;

import com.project.yozmcafe.domain.cafe.Cafe;

public record CafeSearchResponse(Long id, String name, String address, String image, int likeCount) {
public static CafeSearchResponse from(final Cafe cafe) {
return new CafeSearchResponse(
cafe.getId(),
cafe.getName(),
cafe.getAddress(),
cafe.getRepresentativeImage(),
cafe.getLikeCount()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.project.yozmcafe.controller.dto.cafe;

import com.project.yozmcafe.domain.cafe.Cafe;

public record CafeThumbnailResponse(Long cafeId, String imageUrl) {

public static CafeThumbnailResponse from(final Cafe cafe) {
return new CafeThumbnailResponse(cafe.getId(), cafe.getRepresentativeImage());
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.project.yozmcafe.domain.cafe;

import java.util.List;

public interface CafeCustomRepository {

List<Cafe> findAllBy(final String cafeName, final String menu, final String address);

List<Cafe> findAllBy(final String cafeName, final String address);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.project.yozmcafe.domain.cafe;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.StringPath;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.project.yozmcafe.domain.cafe.QCafe.cafe;
import static com.project.yozmcafe.domain.menu.QMenu.menu;
import static com.querydsl.core.types.dsl.Expressions.numberTemplate;
import static io.micrometer.common.util.StringUtils.isBlank;

@Repository
public class CafeCustomRepositoryImpl extends QuerydslRepositorySupport implements CafeCustomRepository {

private static final double MATCH_THRESHOLD = 0.0;

public CafeCustomRepositoryImpl() {
super(Cafe.class);
}

public List<Cafe> findAllBy(final String cafeNameWord, final String menuWord, final String addressWord) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 쿼리 dsl을 학습하지 않아서 잘은 모르겠지만 제가 이번에 근로 학습을 하면서 봤던 것은 지금처럼 cafeNameWord, menuWord, addressWord에 따라 메소드 오버로딩을 할 필요 없이 한 메소드를 통해서 진행할 수 있었는데 지금은 불가한 것일까요 ?

왜 오버로딩했는지가 궁금합니다! 단순하게 leftJoin, innerJoin 때문에 분리한건지도요 !

Copy link
Collaborator Author

@hum02 hum02 Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오버로딩 했던 이유가 menu검색을 하지 않을 경우에는 cafe엔티티만으로 검색이 가능해서에요!
그래서 분기문으로 menu검색이 없을 때는 불필요한 조인을 하지 않도록 하려 했습니다.

그런데 불필요한 join이라면 db측의 옵티마이저가 알아서 최적화해주지 않을까? 하는 생각도 들어서 50000개 정도의 카페에 메뉴2개씩 저장해서 테스트 해봤어요
image
image

알아서 최적화 해주지는 않네요.. join시 메뉴 개수만큼 곱해져서 조회되기에 그만큼의 성능 차이가 납니다.
결론은 leftjoin,inner join이냐의 문제보다는 join이 필요없을 때는 안하는게 게 성능 상 좋아서 분리하려해요!

return from(cafe)
.innerJoin(menu).on(menu.cafe.eq(cafe))
.where(
contains(cafe.name, cafeNameWord),
contains(menu.name, menuWord),
contains(cafe.address, addressWord))
.fetch();
}

public List<Cafe> findAllBy(final String cafeNameWord, final String addressWord) {
return from(cafe)
.where(
contains(cafe.name, cafeNameWord),
contains(cafe.address, addressWord))
.fetch();
}

private BooleanExpression contains(final StringPath target, final String searchWord) {
if (isBlank(searchWord)) {
return null;
}

final String formattedSearchWord = "\"" + searchWord + "\"";
return numberTemplate(Double.class, "function('match_against', {0}, {1})",
target, formattedSearchWord)
.gt(MATCH_THRESHOLD);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import java.util.List;
import java.util.Optional;

public interface CafeRepository extends JpaRepository<Cafe, Long> {
public interface CafeRepository extends JpaRepository<Cafe, Long>, CafeCustomRepository {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 이런 구조를 생각했었는데 반영 👍


Slice<Cafe> findSliceBy(Pageable pageable);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.project.yozmcafe.domain.cafe;

import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.FunctionContributor;

import static org.hibernate.type.StandardBasicTypes.DOUBLE;

public class CustomFunctionContributor implements FunctionContributor {

private static final String FUNCTION_NAME = "match_against";
private static final String FUNCTION_PATTERN = "match (?1) against (?2 in boolean mode)";

Comment on lines +8 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RepositoryImpl에서 match_against라는 것을 봤을 때 이게 뭐지?? 싶었는데 패턴 매칭으로 여기서 함수를 정의하고 있었군요.

match against 문을 바로 쓰지 않고 함수화하는 이유가 궁금합니다.
어떤 이점이 있나요??

Copy link
Collaborator Author

@hum02 hum02 Sep 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 가능하면 특화구문을 최소한의 의존성으로 사용하고 싶어요!

 private BooleanExpression contains(final StringPath target, final String searchWord) {
        if (isBlank(searchWord)) {
            return null;
        }
        return booleanTemplate("match({0}) against ({1} in boolean mode) > 0", target, searchWord);
    }

이런 식으로 match against문을 바로 쓰는 것을 생각하신 게 맞나요?
이렇게 사용할 시 hql이 이런 특화 구문을 지원해주지 않아 hibernate에서 parsing할 때 exception이 발생합니다 ㅜ.
image

그렇다고 native query사용을 위해 entityManager를 직접 사용하면 querydsl을 통한 동적쿼리의 의미가 없어지는 것 같고....
때문에 함수 등록을 하는 방법을 택했습니다! 다른 방법이 있을까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

query dsl은 말 그대로 DSL로 편하게 동적 쿼리를 작성하기 위함이고 native query를 사용하는 것은 피할 수 없다고 생각해요.
필요하면 JDBC Template을 쓰거나, Entity Manager로 날리거나 하는 것이 당연하다고 생각합니다!

근데 만약 여기서 Entity Manager를 사용하면 좀 더 추상객체(Entity Manager)의 의존만으로 처리할 수 있지 않나 해서 남겼어요!
어떤 장점 때문에 했는지 궁금해서요.

native query가 필요할 때마다 native query가 아닌 Query DSL로 하실 것인가요??

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 Entity Manager로 변경하라는 뜻은 아닙니다!
지금 그대로도 좋아요!! 단순 궁금증입니다.

Copy link
Collaborator Author

@hum02 hum02 Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

querydsl의 where에 native쿼리가 들어가야 처음 querydsl을 쓸 때 의도했던 대로
하나의 쿼리 template으로 동적인 쿼리가 가능하다 생각해요!

Entity Manager를 쓰면서 이를 querydsl에 적용하려면 EntityManager의 쿼리 형식을 booleanExpression으로 바꿔야 하는 데 그게 가능한지 모르겠네요...

@Override
public void contributeFunctions(final FunctionContributions functionContributions) {
functionContributions.getFunctionRegistry()
.registerPattern(FUNCTION_NAME, FUNCTION_PATTERN,
functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(DOUBLE));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import jakarta.persistence.OrderBy;

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

import static com.project.yozmcafe.exception.ErrorCode.NOT_EXISTED_LIKED_CAFE;
Expand Down Expand Up @@ -100,14 +99,13 @@ public List<UnViewedCafe> getNextUnViewedCafes(final int size) {
return result;
}

public List<LikedCafe> getLikedCafesSection(final int startIndex, final int endIndex) {
if (startIndex >= likedCafes.size()) {
return Collections.emptyList();
}

final List<LikedCafe> reverseLikedCafes = new ArrayList<>(likedCafes);
public List<Cafe> getLikedCafes(final int startIndex, final int endIndex) {
final List<LikedCafe> likedCafes = new ArrayList<>(this.likedCafes);
final List<Cafe> cafes = likedCafes.stream()
.map(LikedCafe::getCafe)
.toList();

return reverseLikedCafes.subList(startIndex, min(endIndex, likedCafes.size()));
return cafes.subList(startIndex, min(endIndex, this.likedCafes.size()));
}

public String getId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.project.yozmcafe.controller.dto.cafe.CafeRankResponse;
import com.project.yozmcafe.controller.dto.cafe.CafeResponse;
import com.project.yozmcafe.controller.dto.cafe.CafeSearchRequest;
import com.project.yozmcafe.controller.dto.cafe.CafeSearchResponse;
import com.project.yozmcafe.domain.CafeRankGenerator;
import com.project.yozmcafe.domain.cafe.Cafe;
import com.project.yozmcafe.domain.cafe.CafeRepository;
Expand All @@ -15,6 +17,7 @@
import java.util.List;

import static com.project.yozmcafe.exception.ErrorCode.NOT_EXISTED_CAFE;
import static io.micrometer.common.util.StringUtils.isBlank;

@Service
@Transactional(readOnly = true)
Expand Down Expand Up @@ -70,4 +73,19 @@ public CafeResponse getCafeById(final long cafeId) {

return CafeResponse.fromUnLoggedInUser(foundCafe);
}

public List<CafeSearchResponse> getCafesBySearch(final CafeSearchRequest searchRequest) {
final List<Cafe> cafes = searchWith(searchRequest);

return cafes.stream()
.map(CafeSearchResponse::from)
.toList();
}

private List<Cafe> searchWith(final CafeSearchRequest cafeSearchRequest) {
if (isBlank(cafeSearchRequest.menu())) {
return cafeRepository.findAllBy(cafeSearchRequest.cafeName(), cafeSearchRequest.address());
}
return cafeRepository.findAllBy(cafeSearchRequest.cafeName(), cafeSearchRequest.menu(), cafeSearchRequest.address());
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.project.yozmcafe.service;

import com.project.yozmcafe.controller.dto.cafe.CafeThumbnailResponse;
import com.project.yozmcafe.controller.dto.cafe.LikedCafeResponse;
import com.project.yozmcafe.controller.dto.cafe.LikedCafeThumbnailResponse;
import com.project.yozmcafe.domain.cafe.Cafe;
import com.project.yozmcafe.domain.cafe.CafeRepository;
import com.project.yozmcafe.domain.cafe.LikedCafe;
Expand All @@ -26,23 +26,16 @@ public LikedCafeService(final CafeRepository cafeRepository, final MemberService
this.memberService = memberService;
}

public List<LikedCafeThumbnailResponse> findLikedCafeThumbnailsByMemberId(final String memberId, final Pageable pageable) {
public List<CafeThumbnailResponse> findLikedCafeThumbnailsByMemberId(final String memberId, final Pageable pageable) {
final Member member = memberService.findMemberByIdOrElseThrow(memberId);

final List<LikedCafe> likedCafes = getLikedCafes(pageable, member);
final List<Cafe> likedCafes = member.getLikedCafes((int) pageable.getOffset(), pageable.getPageSize());

return likedCafes.stream()
.map(LikedCafeThumbnailResponse::from)
.map(CafeThumbnailResponse::from)
.toList();
}

private List<LikedCafe> getLikedCafes(final Pageable pageable, final Member member) {
final int startIndex = pageable.getPageNumber() * pageable.getPageSize();
final int endIndex = startIndex + pageable.getPageSize();

return member.getLikedCafesSection(startIndex, endIndex);
}

public List<LikedCafeResponse> findLikedCafesByMemberId(final String memberId) {
final Member member = memberService.findMemberByIdOrElseThrow(memberId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.project.yozmcafe.domain.cafe.CustomFunctionContributor
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수를 등록하려면 이 부분이 필요한 건가요??
어떤 부분인지 궁금해요

Copy link
Collaborator Author

@hum02 hum02 Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

여기서 fuctionContirbutor를 사용하려면 sericeLoader에서 스캔하도록 META-INF경로에 파일을 만들거나
코드를 통해 등록하려면 applyFunctions등을 이용하라 안내하는데요.
첫번째 방법을 위해 serviceLoader가 스캔하는 경로에 파일을 등록했습니다.

두번 째 방법인
image
이 방식으로 해보니 여전히 직접 설정을 집어넣어야 하는 게 존재해서 이러느니 META-INF에 파일을 만드는 지금 방식이 더 낫다고 생각합니다

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE cafe ADD FULLTEXT INDEX cafe_name_idx (name) WITH PARSER NGRAM;

ALTER TABLE cafe ADD FULLTEXT INDEX address_idx (address) WITH PARSER NGRAM;

ALTER TABLE menu ADD FULLTEXT INDEX menu_name_idx (name) WITH PARSER NGRAM;
Loading