반응형
11-30 07:35
Today
Total
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
관리 메뉴

개발하는 고라니

[JPA] Querydsl 기본 본문

Framework/JPA (Hibernate)

[JPA] Querydsl 기본

조용한고라니 2021. 11. 20. 19:52
반응형

Querydsl

Querydsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 생성할 수 있도록 해주는 프레임워크이다. 또한 Querydsl은 JPQL 빌더이다.

 

현재 JPA를 JPQL -> Spring Data JPA -> Querydsl순으로 배우고 있다. Querydsl은 무엇이고 왜 쓰이는지, 어떻게 쓰는지 JPQL과 비교하며 알아본다.

 

JPQL을 사용하다보면 한계에 부딪힐 때가 있다.

 

1) 복잡한 쿼리

- JPQL에서 복잡한 쿼리를 타이핑 중 오타가 날 수도 있고, 가독성이 떨어질 수 있다.

- JPQL에서 파라미터를 .setParameter(name, value)를 통해 바인딩해야한다.

 

- Querydsl에서 java 코드를 사용하기 때문에 컴파일 시점에 타입 체크나 오타를 잡아주어 IDE의 도움을 받을 수 있다. 반면 JPQL는 문자열로 쿼리를 짜기 때문에 컴파일 시점에 디버깅이 불가능하다. 물론 요즘은 IDE가 좋아져 일부 JPQL의 에러는 IDE의 도움을 받을 수 있다.

- Querydsl에서 파라미터를 바인딩 할 때 바로 꽂아 넣을 수 있어 간편하고 가독성이 좋다.

 

예) User, Board, Reply가 있다고 할 때, 댓글을 조회하는데 댓글의 내용과 게시글의 제목, 댓글을 쓴 사람의 이름으로 필터링 해서 조회하는 쿼리를 짜보자.

> JPQL

    @Test
    @DisplayName("댓글 조회 테스트_JPQL")
    void read_replies_test() throws Exception {
        //given
        String username = "name";
        String title = "title";
        String content = "content";
        //when
        String query = "select r "
            + "from Reply r "
            + "left join fetch r.board b "
            + "left join fetch r.user u "
            + "left join fetch r.parent p "
            + "where ( "
            + "b.title = :title "
            + "and u.name = :username "
            + "and r.content = :content "
            + " ) "
            + "order by r.id desc";

        List<Reply> replies = em.createQuery(query, Reply.class)
            .setParameter("username", username)
            .setParameter("title", title)
            .setParameter("content", content)
            .getResultList();
        //then
    }

 

> Querydsl

    @Test
    @DisplayName("댓글 조회 테스트_Querydsl")
    void read_replies_test_querydsl() throws Exception {
        //given
        String username = "name";
        String title = "title";
        String content = "content";
        //when
        QReply parent = new QReply("p");
        List<Reply> replies = factory
            .selectFrom(reply)
            .leftJoin(reply.board, board).fetchJoin()
            .leftJoin(reply.user, user).fetchJoin()
            .leftJoin(reply.parent, parent).fetchJoin()
            .where(
                user.name.eq(username),
                board.title.eq(title),
                reply.content.eq(content)
            )
            .orderBy(reply.id.desc())
            .fetch();
        //then
    }

 

개인적으로 둘 다 mybatis를 이용할 때보다 훨씬 편한 것은 맞지만, querydsl을 써보니 확실히 더 편하고, 깔끔한 것을 느낀다.

 

2) 동적 쿼리

- JPQL에서 동적 쿼리를 사실상 쓰기가 불가능하다고 해도 과언이 아니다. Criteria라는 것을 이용해야하는데, 이걸 사용하라고 만든 건지 의문이 들을 정도로 복잡하다. 그래서 써본적도 없고 앞으로도 쓸 일은 없을 것이다.

 

- Querydsl은 BooleanBuilder나 다중 파라미터 등을 이용해 동적쿼리를 손쉽게 사용할 수 있다.

 

> Querydsl

    @Test
    @DisplayName("동적 쿼리")
    void dynamicQuery_test() throws Exception {
        //given
        String username = "name";
        String title = "title";
        String content = "content";
        //when
        QReply parent = new QReply("p");
        List<Reply> replies = factory
            .selectFrom(reply)
            .leftJoin(reply.board, board).fetchJoin()
            .leftJoin(reply.user, user).fetchJoin()
            .leftJoin(reply.parent, parent).fetchJoin()
            .where(
                dynamicSearch(username, title, content)
            )
            .orderBy(reply.id.desc())
            .fetch();
        //then
    }

    private BooleanBuilder dynamicSearch(String username, String title, String content) {
        BooleanBuilder builder = new BooleanBuilder();

        if(username != null){
            builder.and(user.name.eq(username));
        }
        if(title != null){
            builder.and(board.title.eq(title));
        }
        if(content != null){
            builder.and(reply.content.eq(content));
        }

        return builder;
    }

 

따라서 이러한 문제들을 간단하고 깔끔하게 해결할 수 있는 것이 Querydsl이다. 요약하자면, Querydsl은 쿼리를 자바 코드로 작성해 컴파일 시점에 문법 오류를 잡아주며, 동적 쿼리 문제를 해결할 수 있고, 쉬운 SQL 스타일의 문법으로 쉽게 학습할 수 있다.

참고사항

※ Querydsl을 사용하기 위해선 JPAQueryFactory를 사용해야하는데, 이를 각 메서드에서 생성해 사용하는 것보다 Class의 필드로 선언해놓고 쓰는 것이 권장된다. 멀티 스레드환경에서도 동시성 문제가 발생하지 않게 구현되어 있기 때문에 이는 걱정하지 않아도 된다고 한다.

class SampleQuerydslRepository {

    @PersistenceContext
    private EntityManager em;
    
    private JPAQueryFactory factory;
    ...
}

 

※ Querydsl은 특이하게 Q-Type을 사용하는데, 이를 static import 해놓고 사용하면 편하다.

import gorany.dslshop.entity.QBoard;
import gorany.dslshop.entity.QReply;
import gorany.dslshop.entity.QUser;
...

 

※ 앞으로의 예제에 사용될 도메인 간의 관계이다.

조회

  • fetch() : List 조회, 데이터가 없으면 빈 List 반환
  • fetchOne() : 단 건 조회
    • 결과가 없을 때 : null
    • 결과가 2개 이상일 때 : NonUniqueResultException 발생
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보를 포함하여 조회되며, 전체 개수가 필요하기 때문에 count query가 실행된다.
  • fetchCount() : count 쿼리로 변경해서 count 수 조회
//fetch
        String query = "select u from User u";
        List<User> users_jpql = em.createQuery(query, User.class)
            .getResultList();

        List<User> users_querydsl = factory
            .selectFrom(user)
            .fetch();
            
//fetchOne
        User user_jpql = em.createQuery(query, User.class)
            .getSingleResult();
        
        User user_querydsl = factory
            .selectFrom(QUser.user)
            .fetchOne();
            
//fetchFirst
        User user_jpql = em.createQuery(query, User.class)
            .setMaxResults(1)
            .getSingleResult();

        User user_querydsl = factory
            .selectFrom(QUser.user)
            .fetchFirst();
            
//fetchResults
/*
복잡한 쿼리의 경우 fetchResults()를 사용하지 말고, 
데이터를 가져오는 것과 count를 가져오는 쿼리를 다르게 써야할 수도 있다.
(성능 문제)
*/
        //JPQL은 안되고 Spring Data JPA를 써야 비슷하게 가능
        
        QueryResults<User> users_querydsl = factory
            .selectFrom(user)
            .fetchResults();
            
//fetchCount
        long count_jpql = em.createQuery("select count(u) from User u", Long.class)
            .getSingleResult();

        long count_querydsl = factory
            .selectFrom(user)
            .fetchCount();

페이징

페이징의 경우 복잡한 것 말고 비교적 간단한 쿼리를 작성해서, 카운트 쿼리와 조회 쿼리를 동일하게 사용한다는 가정을 한다.

> JPQL

    @Test
    @DisplayName("paging JPQL")
    void paging_jpql_test() throws Exception {
        //given
        String content = "reply%";
        //when
        List<Reply> result = em.createQuery("select r from Reply r "
                + "join fetch r.user u "
                + "join fetch r.board b "
                + "where r.content like :content ", Reply.class)
            .setParameter("content", content)
            .setFirstResult(0)
            .setMaxResults(10)
            .getResultList();
        //then
        assertThat(result.size()).isEqualTo(10);
        assertThat(result.get(0).getContent()).contains("reply");
    }

 

> Querydsl

> fetch

    @Test
    @DisplayName("paging")
    void paging_test() throws Exception {
        //given
        String content = "reply";
        //when
        List<Reply> result = factory
            .selectFrom(reply)
            .join(reply.user, user).fetchJoin()
            .join(reply.board, board).fetchJoin()
            .where(reply.content.startsWith(content))
            .offset(0)
            .limit(10)
            .fetch();
        //then
        assertThat(result.size()).isEqualTo(10);
        assertThat(result.get(0).getContent()).contains("reply");
    }

 

> fetchResults

    @Test
    @DisplayName("paging fetchResults")
    void paging_fetchResults_test() throws Exception {
        //given
        String content = "reply";
        //when
        QueryResults<Reply> result = factory
            .selectFrom(reply)
            .join(reply.user, user).fetchJoin()
            .join(reply.board, board).fetchJoin()
            .where(reply.content.startsWith(content))
            .offset(0)
            .limit(10)
            .fetchResults();
        //then
        assertThat(result.getTotal()).isEqualTo(50);
        assertThat(result.getResults().get(0).getContent()).contains("reply");
    }

조인

조인에는 inner join, left/right join이 있으며, 연관관계가 없는 필드로 조인하는 '세타 조인'이 있다. JPQL의 on과, 성능 최적화를 위한 fetch join을 지원한다.

//inner fetch join
        List<Board> result_inner = factory
            .selectFrom(board)
            .innerJoin(board.user, user).fetchJoin()
            .fetch();

//left fetch join
        List<Board> result_left = factory
            .selectFrom(board)
            .leftJoin(board.user, user).fetchJoin()
            .fetch();
            
//inner join
        List<Board> result_inner = factory
            .selectFrom(board)
            .innerJoin(board.user, user)
            .fetch();
            
//left join
        List<Board> result_left = factory
            .selectFrom(board)
            .leftJoin(board.user, user)
            .fetch();

세타 조인

하다보면 연관관계가 없는 데이터에 대해서도 조인해서 사용해야할 때가 있을 수 있다. (난 아직없었다)

이를 해결할 수 있는 방법이 2가지 정도 있다. 하나는 where 절에서 처리하는 것이고, 나머지는 join에 'on'을 사용하는 것이다.

 

> where 절 (cross join)

※ 이때 주의할 사항은 from 절에 여러 엔티티를 선택해야한다.

    @Test
    @DisplayName("theta join")
    void theta_join_where_test() throws Exception {
        //given

        //when
        //댓글의 내용과 게시글의 내용이 같은 것을 조회한다.
        List<Reply> result = factory
            .select(reply)
            .from(reply, board)
            .where(reply.content.eq(board.content))
            .fetch();
        //then
        /*
        * 실행된 SQL
        select
            reply0_.reply_id as reply_id1_3_,
            reply0_.board_id as board_id3_3_,
            reply0_.content as content2_3_,
            reply0_.parent_id as parent_i4_3_,
            reply0_.user_id as user_id5_3_ 
        from
            reply reply0_ cross 
        join
            board board1_ 
        where
            reply0_.content=board1_.content
        * */
    }

 

> on 사용 (inner join 혹은 left/right join)

    @Test
    @DisplayName("using on join")
    void using_join_on_test() throws Exception {
        //given

        //when
        List<Tuple> result = factory
            .select(reply, board)
            .from(reply)
            //이 부분을 잘 봐야한다. leftJoin()에 하나의 엔티티만 들어간다.
            //leftJoin(reply.board, board) ==> 이건 ID로 ON절이 매핑되어있다.
            .leftJoin(board).on(reply.content.eq(board.content))
            .fetch();
        //then
        result.forEach(System.out::println);
        /*
        * 실행된 SQL
        select
            reply0_.reply_id as reply_id1_3_0_,
            board1_.board_id as board_id1_0_1_,
            reply0_.board_id as board_id3_3_0_,
            reply0_.content as content2_3_0_,
            reply0_.parent_id as parent_i4_3_0_,
            reply0_.user_id as user_id5_3_0_,
            board1_.content as content2_0_1_,
            board1_.title as title3_0_1_,
            board1_.user_id as user_id4_0_1_
        from
            reply reply0_
        left outer join
            board board1_
                on (
                    reply0_.content=board1_.content
                )
        * */
    }

 

※ ON은 이외에도 조인 대상 필터링을 할 수 있다. 예를 들어 reply와 board를 조회하는데, board의 title이 "board1"인 것만 조회해보자.

 

> on을 이용해 조인 대상 필터링

    @Test
    @DisplayName("조인 대상 필터링 using on")
    void filtering_using_on() throws Exception {
        //given
        String boardTitle = "board1";
        //when
        List<Tuple> result = factory
            .select(reply, board)
            .from(reply)
            .leftJoin(reply.board, board).on(board.title.eq(boardTitle))
            .fetch();
        //then
        result.forEach(System.out::println);
        /*
        * 실행된 SQL
        select
            reply0_.reply_id as reply_id1_3_0_,
            board1_.board_id as board_id1_0_1_,
            reply0_.board_id as board_id3_3_0_,
            reply0_.content as content2_3_0_,
            reply0_.parent_id as parent_i4_3_0_,
            reply0_.user_id as user_id5_3_0_,
            board1_.content as content2_0_1_,
            board1_.title as title3_0_1_,
            board1_.user_id as user_id4_0_1_ 
        from
            reply reply0_ 
        left outer join
            board board1_ 
                on reply0_.board_id=board1_.board_id 
                and (
                    board1_.title=?
                )
        * */
        
        /*
        * 출력 결과
        [Reply(id=1, content=reply0), Board(id=1, title=board1, content=board_content1)]
        [Reply(id=2, content=reply1), null]
        [Reply(id=3, content=reply2), null]
        [Reply(id=4, content=reply3), null]
        [Reply(id=5, content=reply4), null]
        [Reply(id=6, content=reply5), null]
        [Reply(id=7, content=reply6), null]
        [Reply(id=8, content=reply7), null]
        ...
        * */
    }

 

※ 사실 on으로 조인 대상을 필터링할 때 inner join을 쓰면 where을 사용하는 것과 동일하다.

    @Test
    @DisplayName("filtering using where")
    void filtering_using_where() throws Exception {
        //given

        //when
        List<Tuple> result = factory
            .select(reply, board)
            .from(reply)
            .innerJoin(reply.board, board)
            .where(board.title.eq("board1"))
            .fetch();
        //then
        result.forEach(System.out::println);

        /*
        * 실행된 SQL
        select
            reply0_.reply_id as reply_id1_3_0_,
            board1_.board_id as board_id1_0_1_,
            reply0_.board_id as board_id3_3_0_,
            reply0_.content as content2_3_0_,
            reply0_.parent_id as parent_i4_3_0_,
            reply0_.user_id as user_id5_3_0_,
            board1_.content as content2_0_1_,
            board1_.title as title3_0_1_,
            board1_.user_id as user_id4_0_1_
        from
            reply reply0_
        inner join
            board board1_
                on reply0_.board_id=board1_.board_id
        where
            board1_.title=?
        * */

        /*
        * 출력 결과
        [Reply(id=1, content=reply0), Board(id=1, title=board1, content=board_content1)]
        [Reply(id=9, content=reply8), Board(id=1, title=board1, content=board_content1)]
        [Reply(id=17, content=reply16), Board(id=1, title=board1, content=board_content1)]
        [Reply(id=25, content=reply24), Board(id=1, title=board1, content=board_content1)]
        [Reply(id=33, content=reply32), Board(id=1, title=board1, content=board_content1)]
        [Reply(id=41, content=reply40), Board(id=1, title=board1, content=board_content1)]
        [Reply(id=49, content=reply48), Board(id=1, title=board1, content=board_content1)]
        * */
    }

 

#References

https://velog.io/@tigger/QueryDSL

인프런 김영한님의 실전! Querydsl 강의

반응형

'Framework > JPA (Hibernate)' 카테고리의 다른 글

[JPA] Spring Data JPA  (0) 2021.10.09
[JPA] OSIV (Open Session In View)  (2) 2021.09.19
[JPA] 벌크 연산 (Bulk Operation)  (0) 2021.08.05
[JPA] 페치 조인 (FETCH JOIN)  (0) 2021.08.05
[JPA] Cascade / OrphanRemoval  (0) 2021.07.25
Comments