아래 코드들은 계층형 댓글을 만들며 사용한 QueryDsl 예제이다.
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
-> JPA에서 사용하기 위해 JPAQueryFactory를 DI 받을 수 있게 설정하였다.
원래 있던 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을 사용하는 것이다. 사용할 메서드를 인터페이스에 정의하였다.
import static com.main36.pikcha.domain.comment.entity.QComment.comment;
@Repository
public class CommentCustomRepositoryImpl implements CommentCustomRepository{}
인터페이스 구현 클래스의 이름은 인터페이스 이름 + Impl 이어야 한다
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. 별칭 직접 지정
QMember m1 = new QMember("m");
Member member1 = queryFactory
.selectFrom(m1)
.where(m1.name.eq("member1"))
.fetchOne();
// 2. 기본 인스턴스 사용
QMember m2 = QMember.member;
Member member2 = queryFactory
.selectFrom(m2)
.where(m2.name.eq("member1"))
.fetchOne();
// 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을 안다면 어떤 역할을 하는지 짐작할 수 있을 것이다.
// 원하는 쿼리 문을 날리고 조회하는 메서드
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
위의 fetchJoin()과는 의미가 다른데, 여기서의 fetch()는 QueryDsl의 결과를 반환받는 메서드이다. 크게 5가지의 종류가 있다
fetch() : 리스트로 결과를 반환하는 방법 (만약에 데이터가 없으면 빈 리스트를 반환)
fetchOne() : 단건을 조회할 때 사용하는 방법 (결과가 없을때는 null 을 반환하고 결과가 둘 이상일 경우에는 NonUniqueResultException을 던짐)
fetchFirst() : 처음의 한 건을 쿼리해서 가져오고 싶을때 사용한다
fetchResults() : 페이징을 위해서 total contents를 가져온다
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