[Spring JPA] QueryDsl 맛보기

SangYu Lee·2023년 2월 22일
0

🚀 QueryDsl 세팅 후 간단 사용법

  1. JPAQueryFactory를 Bean으로 등록하여 프로젝트 전역에서 QueryDSL을 작성할 수 있도록 한다
  2. repository 인터페이스를 정의한다
  3. 인터페이스를 구현할 Impl 클래스를 만든다
  4. Impl 클래스 내에서 QueryDsl을 사용한다.

아래 코드들은 계층형 댓글을 만들며 사용한 QueryDsl 예제이다.

1. QueryDsl Configuration

@Configuration
public class QueryDslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}

-> JPA에서 사용하기 위해 JPAQueryFactory를 DI 받을 수 있게 설정하였다.

2. Repository 인터페이스 정의

원래 있던 repository

public interface CommentRepository extends JpaRepository<Comment, Long> {
	@Query(value = "select c from Comment as c where c.post.postId =: postId")
    List<Comment> findCommentByPostId(long postId);
}

새로 만든 repository interface

public interface CommentCustomRepository {
    List<Comment> findCommentByPost(Post post);
}

원래 Repository에서는 postId를 조건으로 댓글 리스트를 조회하는 메서드였는데 계층형 댓글을 구현하면서 생성날짜, 부모 댓글 순서로 정렬 등 쿼리가 많이 복잡해져서 QueryDsl을 사용하는 것이다. 사용할 메서드를 인터페이스에 정의하였다.

3. 인터페이스를 구현한 Impl 클래스 정의

import static com.main36.pikcha.domain.comment.entity.QComment.comment;

@Repository
public class CommentCustomRepositoryImpl implements CommentCustomRepository{}

인터페이스 구현 클래스의 이름은 인터페이스 이름 + Impl 이어야 한다

4. Impl 클래스 내에서 QueryDsl을 사용

import static com.main36.pikcha.domain.comment.entity.QComment.comment;

@Repository
public class CommentCustomRepositoryImpl implements CommentCustomRepository{
	// queryDsl 핵심
    private JPAQueryFactory jpaQueryFactory;

	// 생성자 DI
    public CommentCustomRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
        this.jpaQueryFactory = jpaQueryFactory;
    }
    
    // 원하는 쿼리 문을 날리고 조회하는 메서드
	private List<Comment> findCommnetList(Post post){
        return jpaQueryFactory.selectFrom(comment)
                .leftJoin(comment.parent)
                .fetchJoin()
                .where(comment.post.postId.eq(post.getPostId()))
                .orderBy(comment.parent.commentId.asc().nullsFirst(), comment.createdAt.asc())
                .fetch();
    }
}

Point ( •̀ ω •́ )✧

  • Q Class를 사용할 수 있게 해주어야 한다
  • JPAQueryFactory를 DI 받아야한다

Q Class를 사용하는 방법 (예제는 가져왔다)

  1. 별칭 직접 지정하여 사용하기
// 1. 별칭 직접 지정
QMember m1 = new QMember("m");
Member member1 = queryFactory
                .selectFrom(m1)
                .where(m1.name.eq("member1"))
                .fetchOne();
  1. 기본 인스턴스 사용하기
// 2. 기본 인스턴스 사용
QMember m2 = QMember.member; 
Member member2 = queryFactory
                .selectFrom(m2)
                .where(m2.name.eq("member1"))
                .fetchOne();
  1. static import하여 사용하기
// 3. 직접 static import 하여 사용  
import static kbds.querydsl.domain.QMember.member;

Member member3 = queryFactory
                .selectFrom(member)
                .where(member.name.eq("member1"))
                .fetchOne();

JPAQueryFactory
혹시 눈치가 빠른 사람은 인터페이스를 정의할 때 이상한 점을 느꼈을... 수도 있다
바로 인터페이스를 정의할 때, 그 어떤 것도 implements 하거나 extends 하지도 않고 annotation을 사용하지도 않았다.

public interface CommentCustomRepository {
    List<Comment> findCommentByPost(Post post);
}

완전 깔끔한 인터페이스이다. 그런데도 구현체가 쿼리를 날리고 조회하는 기능을 할 수 있는 이유는 JPAQueryFactory를 DI받았기 때문이다. 이 JPAQueryFactory가 기능을 다 가지고 있어서 JPARepository를 구현하지 않아도 QueryDsl을 사용할 수 있다.


여기서부터는 쿼리를 날렸던 findCommentList 메서드 안에 있는 체이닝 메서드 중 fetchJoin()과 fetch()에 대해서 설명하려고 한다. 나머지는 SQL을 안다면 어떤 역할을 하는지 짐작할 수 있을 것이다.

fetchJoin()

// 원하는 쿼리 문을 날리고 조회하는 메서드
	private List<Comment> findCommnetList(Post post){
        return jpaQueryFactory.selectFrom(comment)
                .leftJoin(comment.parent)
                // fetchJoin -> 연관된 엔티티 모두 한번에 조회
                .fetchJoin()
                .where(comment.post.postId.eq(post.getPostId()))
                .orderBy(comment.parent.commentId.asc().nullsFirst(), comment.createdAt.asc())
                .fetch();
    }

Join, Fetch Join 차이점

🚩일반 Join
Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는
오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화

🏴Fetch Join
연관 Entity도 함께 SELECT 하여 모두 영속화. FetchType이 Lazy인 Entity를 참조하더라도 이미 영속성 컨텍스트에 들어있기 때문에 따로 쿼리가 실행되지 않은 채로 N+1문제가 해결됨

출처 : https://cobbybb.tistory.com/18

fetch()

위의 fetchJoin()과는 의미가 다른데, 여기서의 fetch()는 QueryDsl의 결과를 반환받는 메서드이다. 크게 5가지의 종류가 있다

  1. fetch() : 리스트로 결과를 반환하는 방법 (만약에 데이터가 없으면 빈 리스트를 반환)

  2. fetchOne() : 단건을 조회할 때 사용하는 방법 (결과가 없을때는 null 을 반환하고 결과가 둘 이상일 경우에는 NonUniqueResultException을 던짐)

  3. fetchFirst() : 처음의 한 건을 쿼리해서 가져오고 싶을때 사용한다

  4. fetchResults() : 페이징을 위해서 total contents를 가져온다

  5. fetchCount() : count 쿼리

참고 :
https://devocean.sk.com/blog/techBoardDetail.do?ID=163915
https://jaehoney.tistory.com/211
https://chilling.tistory.com/49
https://cobbybb.tistory.com/18

profile
아이스커피

0개의 댓글