기본 문법

OneTwoThree·2023년 8월 11일
0

실전querydsl

목록 보기
3/6

출처


JPQL vs querydsl

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @Autowired
    EntityManager em;

    @BeforeEach
    public void before(){
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1",10,teamA);
        Member member2 = new Member("member2",20,teamA);

        Member member3 = new Member("member3",30,teamB);
        Member member4 = new Member("member4",40,teamB);
        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);
    }

    @Test
    public void startJPQL(){
        // member1 찾기
        Member findMember = em.createQuery("select m from Member m " +
                        "where m.username = :username", Member.class)
                .setParameter("username", "member1")
                .getSingleResult();

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }

    @Test
    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        QMember m = new QMember("m");

        Member findMember = queryFactory.
                select(m)
                .from(m)
                .where(m.username.eq("member1"))
                .fetchOne();
        
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}

JPAQueryFactory queryFactory = new JPAQueryFactory(em);
EntityManager를 통해 JPAQueryFactory를 가져와야 한다.

QMember m = new QMember("m");
m이라는 별칭을 QMember에게 지정하는 것이다.

좋은 점은 JPQL과 달리 파라미터 바인딩을 하지 않아도 알아서 파라미터 바인딩을 해준다는 것이다.

또한 JPQL은 문자를 사용하기 때문에 쿼리문에 오타가 발생하면 실행해서 런타임에 오류를 발견할 수 있다.

반면에 querydsl은 컴파일 시점에 오류를 발견할 수 있다.

    JPAQueryFactory queryFactory;

    @BeforeEach
    public void before(){
        queryFactory = new JPAQueryFactory(em);
        ...

코드를 더 줄일 수 있다. JPAQueryFactory를 필드로 뺄 수 있다.
필드로 빼도 동시성 문제 등이 발생하지 않는다.

기본 Q-type 활용

Qtype을 사용하는 방법은 2가지가 있다.
QMember m = new QMember("m");
와 같이 new를 통해 객체를 생성해서 사용할 수 있다.
QMember m = QMember.member;
또는 이렇게 generated 된 코드에서 제공하는 필드를 사용할 수도 있다.

    public void startQuerydsl() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);
        Member findMember = queryFactory.
                select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}

또는 이렇게 QMember 자체를 static import 해버릴 수도 있다.
이렇게 쓰는 방식이 가장 깔끔하다.

참고로 querydsl은 jql을 작성하는 빌더 역할을 한다. querydsl로 작성한 코드는 결과적으로 jpql이 된다.

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/querydsl
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create
  properties:
     hibernate:
#      show_sql: true
      format_sql: true
      # jpql을 보기 위함 
      use_sql_comments : true
logging.level:
  org.hibernate.SQL: debug
  org.hibernate.type: trace

jpql을 보기 위해서는 application.yml에 위와 같이 use_sql_comments : true를 추가해주면 된다.

같은 테이블을 조인해야 하는 경우 등에만 new로 선언해서 사용하고 그렇지 않은 경우에는 static import를 사용하는 것이 편리하다.

검색 조건 쿼리


JPQL이 제공하는 모든 검색 조건을 이렇게 사용할 수 있다.

    @Test
    public void search(){
        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1").
                        and(member.age.eq(10)))
                .fetchOne();

        assertThat(findMember.getUsername()).isEqualTo("member1");
        assertThat(findMember.getAge()).isEqualTo(10);
    }

위와 같이 메소드 체이닝을 통해 사용할 수 있다.

        Member findMember = queryFactory
                .selectFrom(member)
                .where(member.username.eq("member1"),member.age.eq(10))
                .fetchOne();

and 파라미터인 경우 위와 같이 .and를 쓰지 않고 ,로 이어서 파라미터 형태로 작성할 수 있다.

그리고 중간에 null이 들어가면 null을 무시한다! 그래서 동적 쿼리를 만들 때 아주 유용하다.

결과 조회

    @Test
    public void resultFetch(){
        // Member의 목록을 List로 조회한다
        List<Member> fetch  = queryFactory
                .selectFrom(member)
                .fetch();

        // 단건 조회
        Member fetchOne = queryFactory
                .selectFrom(member)
                .fetchOne();

        // .limit(1).fetchOne()과 같다
        Member fetchFirst = queryFactory
                .selectFrom(member)
                .fetchFirst();

        // results를 통해 다양한 메서드 호출 가능 (페이징에 필요한 것들)
        QueryResults<Member> results = queryFactory
                .selectFrom(member)
                .fetchResults();

 //        long total = results.getTotal();
        List<Member> content = results.getResults();

        // count만 가져온다 select count(member1)
        long total = queryFactory
                .selectFrom(member)
                .fetchCount();
    }

fetchResults()의 경우 totalCount를 가져오기 위해 쿼리가 2방 나간다. (count 쿼리와 content용 쿼리)
total이 있어야 페이징을 할 수 있다

fetchCount()는 count만 가져온다. 이게 따로 있는 이유는 데이터가 복잡한 경우 성능 때문에 fetchResult를 쓰지 말고 fetchCount를 써야 하는 경우가 있다.

정렬

    @Test
    public void sort(){
        em.persist(new Member(null,100));
        em.persist(new Member("member5",100));
        em.persist(new Member("member6",100));

        List<Member> result = queryFactory.
                selectFrom(member)
                .where(member.age.eq(100))
                .orderBy(member.age.desc(), member.username.asc().nullsLast())
                .fetch();

        Member member5 = result.get(0);
        Member member6 = result.get(1);
        Member memberNull = result.get(2);
        assertThat(member5.getUsername()).isEqualTo("member5");
        assertThat(member5.getUsername()).isEqualTo("member6");
        assertThat(member5.getUsername()).isNull();

    }

nullLast말고 nullFirst, desc말고 asc도 있다.

페이징

    @Test
    public  void paging1(){
        List<Member> result = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1) // 앞에서 몇개를 스킵할지
                .limit(2) // 몇개 select 할지 
                .fetch();

        assertThat(result.size()).isEqualTo(2);

    }
    
        @Test
    public  void paging2(){
        QueryResults<Member> queryResults = queryFactory
                .selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1) // 앞에서 몇개를 스킵할지
                .limit(2) // 몇개 select 할지
                .fetchResults();

        assertThat(queryResults.getTotal()).isEqualTo(4); // 총 수
        assertThat(queryResults.getLimit()).isEqualTo(2);
        assertThat(queryResults.getResults().size()).isEqualTo(2);
    }

페이징하기 위해서 offset()으로 앞에서 몇개를 스킵할지(0부터 시작), limit()으로 offset부터 몇개를 뽑아올지 결정한다.
총 수가 필요한 경우 fetchResults()를 사용하면 된다.

집합

group by, having과 같은 집합에 대해 알아보자

    @Test
    public void aggregation(){
        List<Tuple> result = queryFactory.
                select(
                        member.count(),
                        member.age.sum(),
                        member.age.avg(),
                        member.age.max(),
                        member.age.min()
                        )
                .from(member)
                .fetch();

        Tuple tuple = result.get(0);
        assertThat(tuple.get(member.count())).isEqualTo(4);
        assertThat(tuple.get(member.age.avg())).isEqualTo(25);
        assertThat(tuple.get(member.age.max())).isEqualTo(40);
        assertThat(tuple.get(member.age.min())).isEqualTo(10);

    }

이렇게 원하는 속성들을 골라서 select 하면 Tuple로 조회한다 (querydsl에서 제고아는 tuple)
tuple에서 값을 꺼낼 때는 select 절에서 사용한 문법을 그대로 사용하면 된다.

단일 타입이 아니고 이렇게 여러 타입으로 조회할 경우 Tuple을 사용하게 된다.

실무에서는 Tuple을 사용하기 보다 dto로 뽑아오는 방법을 사용한다.

집합

    /**
     *  팀의 이름과 각 팀의 평균 연령을 구해라
     */
    @Test
    void group(){
        List<Tuple> result = queryFactory.
                select(team.name, team, member.age.avg())
                .from(member)
                .join(member.team, team)
                .groupBy(team.name) // 팀의 이름으로 group by
                .fetch();

        Tuple teamA = result.get(0);
        Tuple teamB = result.get(1);

        assertThat(teamA.get(team.name)).isEqualTo("teamA");
        assertThat(teamA.get(member.age.avg())).isEqualTo(15);

        assertThat(teamB.get(team.name)).isEqualTo("teamB");
        assertThat(teamB.get(member.age.avg())).isEqualTo(35);

    }

join 문법은 jpql과 유사하다.
team.name으로 group by 한다.
아래와 같이 groupBy 다음에 having 절도 사용할 수 있다.

...
.groupBy(item.price)
.having(item.price.gt(1000))
...

조인

기본 조인

join(조인 대상, 별칭으로 사용할 Q 타입)

    /**
     * 팀 A에 소속된 모든 회원
     */
    @Test
    void join(){
        List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team) // 두번째 team은 QTeam.team이다(static import)
                .where(team.name.eq("teamA"))
                .fetch();

        assertThat(result)
                .extracting("username")
                .containsExactly("member1","member2");

    }

jpql로 해석된 것을 보면 inner join member1.team as team

leftJoin(), rightJoin() 등도 가능하다.


    @Test
    void theta_join(){
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));

        List<Member> result = queryFactory
                .select(member)
                .from(member, team) // 모든 member랑 team 조인
                .where(member.username.eq(team.name)) // 멤버 이름이랑 팀 이름이 같은 것만 추출
                .fetch();

        assertThat(result)
                .extracting("username")
                .containsExactly("teamA","teamB");
    }

세타 조인은 연관관계가 없는 테이블 끼리 조인을 하는 것이다.
from 절에서 여러 엔티티를 선택해서 세타 조인을 할 수 있다.
하지만 외부 조인이 불가능하다.
외부 조인은 on 절을 사용하면 가능하다.

on절

on절을 활용한 조인은 JPA 2.1부터 지원한다.

    /**
     *  회원과 팀을 조인하면서 팀 이름이 team A인 팀만 조인, 회원은 모두 조회
     *  JPQL : select m, t from Member m left join m.team t on t.name = 'teamA'
     */
    @Test
    void join_on_filtering(){
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(member.team, team)
                .on(team.name.eq("teamA"))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

결과는 아래와 같다

on 절을 활용해서 team 중에 teamA인 팀만 가져와서 join했기 때문에 teamB 소속인 member3와 member4는 team이 null이다.
left join이기 때문에 join의 왼쪽에 해당하는 member는 join 대상이 없어도 다 가져온다.

    @Test
    void join_on_filtering(){
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
                .on(team.name.eq("teamA"))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

위와 같이 일반 join에서도 on절을 사용할 수 있다.
위 결과는

    @Test
    void join_on_filtering(){
        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .join(member.team, team)
               //  .on(team.name.eq("teamA"))
                .where(team.name.eq("teamA"))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

join 후 where절을 사용한 결과와 같다.


정리하자면 on 절로 join 하려는 대상을 줄여서 join할 수 있다. 이것은 left join인 경우에만 의미가 있다. inner join이면 where절에서 걸러버리는 것과 결과가 같다.

즉 inner join인 경우 익숙한 where 절을 사용하고 left join일 때 join 대상을 필터링 해야하는 경우에 on 절을 사용하자.


    @Test
    void join_on_no_relation(){
        em.persist(new Member("teamA"));
        em.persist(new Member("teamB"));

        List<Tuple> result = queryFactory
                .select(member, team)
                .from(member)
                .leftJoin(team).on(member.username.eq(team.name))
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }
    }

연관관계가 없는 엔티티를 외부 조인할 수 있다.
연관관계가 있다면 .leftJoin(team.name) 이런 식으로 작성한다. 이럴 경우 team.name의 id값으로 조인이 된다.
하지만 연관관계가 없는 세타조인이기 때문에 .leftJoin(team).on(member.username.eq(team.name)) 으로 작성한다.
이렇게 할 경우 join하는 조건이 on절에 있는 조건이 된다.

페치 조인

페치 조인은 SQL이 제공하는 기능은 아니고 SQL 조인을 활용해서 연관된 엔티티를 SQL 한 방에 조회하는 기능이다. 주로 성능 최적화에 사용한다.

    @PersistenceUnit
    EntityManagerFactory emf;

        @Test
        void fetchJoinNo(){
            em.flush();
            em.clear();

            // lazy이므로  db에서 조회할 때는 Member만 조회하고 Team은 조회하지 않는다
            Member findMember = queryFactory
                    .selectFrom(member)
                    .where(member.username.eq("member1"))
                    .fetchOne();

            boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
            assertThat(loaded).as("페치 조인 미적용").isFalse();
        }

페치 조인을 미적용하면 Member를 가져올 때 Team은 가져오지 않는다.

        @Test
        void fetchJoinUse(){
            em.flush();
            em.clear();

            // lazy이므로  db에서 조회할 때는 Member만 조회하고 Team은 조회하지 않는다
            Member findMember = queryFactory
                    .selectFrom(member)
                    .join(member.team, team).fetchJoin()
                    .where(member.username.eq("member1"))
                    .fetchOne();

            boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
            assertThat(loaded).as("페치 조인 미적용").isTrue();
        }

fetch join은 그냥 join과 문법은 같은데 join 뒤에 .fetchJoin()을 넣어주면 된다.

서브 쿼리

querydsl에서는 com.querydsl.jpa.JPAExpressions를 사용하면 된다.

    @Test
        void subQuery(){
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(
                        JPAExpressions.
                                select(memberSub.age.max())
                                .from(memberSub)
                ))
                .fetch();

        assertThat(result).extracting("age")
                .containsExactly(40);


    }

서브쿼리인경우는 바깥의 Member랑 alias가 겹치면 안되기 때문에
new Member 로 하나 만들어 줘야 한다.
JPAExpressions로 서브쿼리를 만들어서 사용한다.

    /**
     * 나이가 평균 이상인 회원 조회
     */
    @Test
    void subQueryGoe(){
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.goe(
                        JPAExpressions.
                                select(memberSub.age.avg())
                                .from(memberSub)
                ))
                .fetch();

        assertThat(result).extracting("age")
                .containsExactly(30,40);
    }

서브쿼리에서 goe (greater or equal) 을 사용해 >=를 표현할 수 있다.

    @Test
    void selectSubQuery(){
        QMember memberSub = new QMember("memberSub");

        List<Tuple> result = queryFactory
                .select(member.username,
                        JPAExpressions
                                .select(memberSub.age.avg())
                                .from(memberSub))
                .from(member)
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
            
        }

위와 같이 select 절에서 서브쿼리를 사용해서 member의 평균 나이를 옆에 찍어줄 수도 있다.

그리고 JPAExpressions는 static import가 가능하다.

한계점

JPA의 서브쿼리는 from 절의 서브쿼리가 안된다.
querydsl도 마찬가지다.

이것을 해결하는 방법은

  • 서브쿼리를 조인으로 변경
  • 쿼리를 2번으로 분리해서 실행
  • nativeSQL사용
    가 있다.

case 문

case문은 select와 where절에서 사용 가능하다
when, then으로 사용하는 방법과 casebuilder를 사용하는 방법이 있다.

    @Test
    public void basicCase(){
        List<String> result = queryFactory
                .select(member.age
                        .when(10).then("열살")
                        .when(20).then("스무살")
                        .otherwise("기타")
                ).
                from(member)
                .fetch();

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }

위 예시는 when, then으로 작성하는 간단한 case문이다.

    @Test
    void coplexCase() {
        List<String> result = queryFactory.
                select(new CaseBuilder()
                        .when(member.age.between(0, 20)).then("0~20살")
                        .when(member.age.between(21, 30)).then("21~30살")
                        .otherwise("기타")
                ).from(member)
                .fetch();

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }

CaseBuilder를 사용하는 case 문은 위와 같다.

하지만 db는 row 데이터를 필터링, 그룹핑 정도의 최소한의 연산만 하고 이렇게 보여주기 위한 로직은 db에서 하지 않는게 좋다. 이러한 로직은 애플리케이션, 프레젠테이션 layer에서 해결하는 것이 좋다.

상수, 문자 더하기

    @Test
    void constant(){
        List<Tuple> result = queryFactory.select(member.username, Expressions.constant("A"))
                .from(member)
                .fetch();

        for (Tuple tuple : result) {
            System.out.println("tuple = " + tuple);
        }


위와 같이 상수가 필요하면 사용할 수 있다.

        @Test
        void concat(){
        // {username}_{age}
            List<String> result = queryFactory
                    .select(member.username.concat("_").concat(member.age.stringValue()))
                    .from(member)
                    .where(member.username.eq("member1"))
                    .fetch();

            for (String s : result) {
                System.out.println("s = " + s);
            }
        }

위와 같이 member1_10 과 같은 형태로 결과를 얻을 수 있다.
member.age는 문자열이 아니기 때문에 stringValue()를 사용한다

문자가 아닌 다른 타입들 (enum도) stringValue()를 사용하면 된다

0개의 댓글