지인의 코드를 보면서 공부하던 중, Repository계층에서 JPAQureyFactory
를 사용하여 쿼리를 날리는 것을 보았다. JPQL을 통해서 쿼리문을 작성하는 것이 아닌, JPAQureyFactory
를 사용하여 쿼리를 날리는 이유가 궁금해서 어떤 이유로 쓰고 어떻게 쓰는지 알아보기로 했다!
JPQL로 쿼리를 작성하다 보면, 복잡한 로직은 쿼리 문자열이 상당히 길어진다. 문자열이 길어진다는 의미는 JPQL 문자열에 오타 혹은 문법적인 오류가 발생할 확률이 높아진다는 것을 의미한다. 정적 쿼리라면 어플리케이션 로딩 시점에 이를 발견할 수 있으나 그 외는 런타임 시점에서 에러가 발생하게 된다.
QuryDSL은 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크이다.
즉, JPQL의 문제점을 QureyDSL을 통해서 해결할 수 있다.
JPA의 엔티티를 이용하여 JPQLQurey를 보다 쉽고 편리하게 작성할 수 있는 QureyDSL의 도구이다.
엔티티로 설정된 Q모델이라는 쿼리타입 클래스를 미리 생성해놓고 메타데이터로 사용하여 메소드 기반으로 작성한다.
@Entity
가 붙은 클래스를 찾아 자동으로 생성QueryDSL 설정법, 활용법(검색조건쿼리, 기본 문법들)
해당 글에 예시와 함께 잘 정리되어 있다.
ext {
queryDslVersion = "5.0.0"
}
dependencies {
...
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
// java.lang.NoClassDefFoundError:javax/persistence/Entity 에러 방지
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
...
}
import java.util.Optional;
import com.example.memetory.domain.member.entity.Member;
import com.example.memetory.domain.member.entity.SocialType;
public interface MemberQueryRepository {
Optional<Member> findBySocialTypeAndSocialId(SocialType socialType, String socialId);
Optional<Member> findByEmail(String email);
boolean existsMemberByNickname(String email);
}
Member
에서 사용하는 모든 조회문들을 담은 Interface
import static com.example.memetory.domain.member.entity.QMember.*;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import com.example.memetory.domain.member.entity.Member;
import com.example.memetory.domain.member.entity.SocialType;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
@Repository
@RequiredArgsConstructor
public class MemberQueryRepositoryImpl implements MemberQueryRepository {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Optional<Member> findBySocialTypeAndSocialId(SocialType socialType,
String socialId) {
return Optional.ofNullable(jpaQueryFactory.selectFrom(member)
.where(member.socialType.eq(socialType)
.and(member.socialId.eq(socialId)))
.fetchFirst());
}
@Override
public Optional<Member> findByEmail(String email) {
return Optional.ofNullable(jpaQueryFactory.selectFrom(member)
.where(member.email.eq(email))
.fetchFirst());
}
@Override
public boolean existsMemberByNickname(String nickname) {
return jpaQueryFactory.selectFrom(member).where(member
.nickname.eq(nickname)).fetchOne() != null;
}
}
MemberQureyRepository
을 구현한 클래스이다.JPAQueryFactory
를 활용하여 select
문들을 완성시켰다member
활용 하여 Q-type 객체를 불러왔다.import org.springframework.data.jpa.repository.JpaRepository;
import com.example.memetory.domain.member.entity.Member;
public interface MemberRepository extends JpaRepository<Member, Long>,
MemberQueryRepository {
}
JpaRepository
와 MemberQueryRepository
의 인터페이스들을 상속받아서, JPA 와 QueryDSL을 모두 활용할 수 있다.
여기서 의문이 생긴다. MemberQueryRepository
는 그저 인터페이스인데 어떻게 구현체인 MemberQueryRepositoryImpl
의 QueryDSL을 활용할 수 있는 것 일까?
JpaRepository
인터페이스를 상속받는 Repository
인터페이스를 찾으면, 이를 구현한 프록시 객체를 생성하여 직접 기본 제공 기능을 구현한다.MemberQueryRepository
을 구현한 MemberQueryRepositoryImpl
은 스프링 빈으로 등록된다. MemberQueryRepositoryImpl
이 상속되는 것이다.example : Jin Jin 코드 ➡️ 지인의 코드
/**
* @author 김승진
* @description 멤버 랭킹 응답 정보를 담는 dto
*/
@Getter
@NoArgsConstructor
public class MemberRankResponse {
private UUID id;
private String name;
private String githubId;
private Long tokens;
private Tier tier;
private String profileImage;
@QueryProjection
public MemberRankResponse(
final UUID id,
final String name,
final String githubId,
final Long tokens,
final Tier tier,
final String profileImage) {
this.id = id;
this.name = name;
this.githubId = githubId;
this.tokens = tokens;
this.tier = tier;
this.profileImage = profileImage;
}
}
@QueryProjection
을 통해서 Q-Type 객체를 만들어 직접 사용한다./**
* @author 김승진
* @description 멤버 프로젝션 적용을 위한 클래스
*/
@Component
public class MemberQDtoFactory {
@Bean
public QMemberRankResponse qMemberRankResponse() {
return new QMemberRankResponse(
member.id,
member.name,
member.githubId,
member.sumOfTokens,
member.tier,
member.profileImage);
}
}
select()
에 주입을 해주는 방식으로 구현되어있다.@Override
public List<MemberRankResponse> findRanking(final Pageable pageable) {
return jpaQueryFactory
.select**(qDtoFactory.qMemberRankResponse()**)
.from(member)
.where(member.walletAddress.isNotNull())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(memberOrderConverter.convert(pageable.getSort()))
.fetch();
}
[QueryDsl] Spring JPA와 QueryDsl-JPA
잘보고갑니다~