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

[BE] QueryDSL을 적용한다 #421

Closed
kth990303 opened this issue Sep 9, 2022 · 4 comments · Fixed by #442
Closed

[BE] QueryDSL을 적용한다 #421

kth990303 opened this issue Sep 9, 2022 · 4 comments · Fixed by #442

Comments

@kth990303
Copy link
Collaborator

kth990303 commented Sep 9, 2022

기능 상세

  • 아래와 같은 어마어마한 JPQL을 리팩터링하는 것이 최우선 목표이다.
 @Query(value = "select distinct new com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto"
            + "(m.id, r.id, r.title, t.id, t.name, m.content, m.color, "
            + "case when r.recipient = com.woowacourse.naepyeon.domain.rollingpaper.Recipient.MEMBER then p.nickname "
            + "when r.recipient = com.woowacourse.naepyeon.domain.rollingpaper.Recipient.TEAM then t.name "
            + "else '' end) "
            + "from Message m"
            + ", Rollingpaper r"
            + ", Team t"
            + ", TeamParticipation p "
            + "where m.rollingpaper.id = r.id "
            + "and r.team.id = t.id "
            + "and p.team.id = t.id "
            + "and m.author.id = :authorId "
            + "and (p.member.id = r.member.id or r.member.id is null)")
Page<WrittenMessageResponseDto> findAllByAuthorId(@Param("authorId") final Long authorId,
                                                  final Pageable pageRequest);
  • QueryDSL을 적용함으로써 성능을 향상시키고 복잡한 쿼리의 문법 오류를 컴파일 시점에 확인할 수 있게 하자.
  1. MessageRepository를 제외한 레포지토리는 custom repository impl 로 factory를 들고 있도록
  2. MessageRepository는 JPAQueryFactory를 들고 있는 클래스 생성 -> 바로 DTO로 변환해서 특수한 상황에서만 쓰이기 때문에 별도의 custom repository로 사용 (엔티티로 뽑으면 N+1 문제 발생)
@kth990303 kth990303 self-assigned this Sep 9, 2022
@kth990303 kth990303 added this to To do in Sprint 5 via automation Sep 9, 2022
@kth990303 kth990303 moved this from To do to In progress in Sprint 5 Sep 14, 2022
@kth990303
Copy link
Collaborator Author

queryDSL 트러블슈팅

  • queryDslConfig 클래스를 따로 생성해준 후, 아래 방법대로 하면 No Bean 에러가 뜬다.

QueryDslConfig.class

@Configuration
public class QueryDslConfig {

    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory init() {
        return new JPAQueryFactory(em);
    }
}

MemberRepositoryImpl.class

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

private final JPAQueryFactory queryFactory;

@Override
public Optional<Long> findMemberIdByPlatformAndPlatformId(final Platform platform, final String platformId) {
    return Optional.ofNullable(queryFactory
            .select(member.id)
            .from(member)
            .where(member.platform.eq(platform).and(member.platformId.eq(platformId)))
            .fetchOne());
}
}

결과

Failed to load ApplicationContext
java.lang.IllegalStateException: Failed to load ApplicationContext
  Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'memberRepositoryImpl' defined in file [/Users/kth990303/Desktop/woowacourse/2022-nae-pyeon/backend/build/classes/java/main/com/woowacourse/naepyeon/repository/member/MemberRepositoryImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
		at app//org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
		... 86 more
	Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
		at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1801)
		... 104 more

[Spring Boot에 QueryDSL을 사용해보자](https://tecoble.techcourse.co.kr/post/2021-08-08-basic-querydsl/)

해결 방법

  • @DataJpaTest는 스프링 빈을 모두 등록하지 않고, 엔티티와 EntityManager 정도만 등록한다.

    • QueryDslConfig에서 빈으로 등록해준 JPAQueryFactory 역시 등록해주지 않는다.
    • RepositoryTest에 아래와 같이 해주자
    @Import(QueryDslConfig.class)

@kth990303
Copy link
Collaborator Author

queryDSL 구조

image

출처: 인프런 김영한 - queryDSL 강의자료

@kth990303
Copy link
Collaborator Author

findByXXXId 의 쿼리 성능 개선

연관관계가 있을 경우 특정 엔티티가 아닌 엔티티의 id로 조회하면 join절이 나간다. 따라서 성능 상으로 좋지 않다.

  • Spring Data JPA의 경우
List<TeamParticipation> findByTeamId(final Long teamId);

image

→ XXX의 식별자로 조회하므로 XXX 테이블을 조인해서 가져온다. Spring Data JPA의 기능으로 JPQL이 의도치 않게 추가적으로 호출해서 사용되는 것.

  • QueryDSL로 변경할 경우
@Override
public List<TeamParticipation> findByTeamId(final Long teamId) {
    return queryFactory
            .selectFrom(teamParticipation)
            .where(isTeamIdEq(teamId))
            .fetch();
}

image

→ 정상적으로 left outer join 대신 where절로 처리한다. JPQL을 사용하기 때문

참고

[[Spring Data JPA] findByXXXId 는 불필요한 join을 유발한다](https://velog.io/@ohzzi/Data-Jpa-findByXXXId-%EB%8A%94-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-join%EC%9D%84-%EC%9C%A0%EB%B0%9C%ED%95%9C%EB%8B%A4)

@kth990303
Copy link
Collaborator Author

kth990303 commented Sep 16, 2022

해당 회원이 작성한 메시지 목록 조회하는 쿼리 개선

  • cross join → left outer join -> inner join
  • 트러블슈팅
    • DTO를 반환하므로 Projections을 이용해야 하는데, 해당 클래스에는 count()를 지원해주지 않아 Pagination에 애를 먹음
    • 해결 방법
      • return PageableExecutionUtils.getPage(content, pageRequest, countQuery::fetchOne); 꼴로 반환하기 위해서는 longSupplier(람다식)이 필요한데, Projections는 해당 방법이 불가능하므로 stream을 이용한 count (long타입)을 넣을 수 있도록 return new PageImpl<>(content, pageRequest, total); 반환꼴 사용
return queryFactory
            .select(makeProjections()) // DTO 만들기
            .from(message)
            .join(teamParticipation).on(message.rollingpaper.team.id.eq(teamParticipation.team.id))
            .where(isAuthorIdEq(authorId)
                    .and(message.rollingpaper.member.id.isNull()
                            .or(message.rollingpaper.member.id.eq(teamParticipation.member.id))
                    )
            )
            .distinct();

image

  • left outer join → inner join

Sprint 5 automation moved this from In progress to Done Sep 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

Successfully merging a pull request may close this issue.

1 participant