RE-Heat 개발자 일지

[Querydsl] [3] 기본 문법 (상편) 본문

백엔드/Querydsl

[Querydsl] [3] 기본 문법 (상편)

RE-Heat 2023. 9. 17. 23:18

https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84/dashboard

 

실전! Querydsl - 인프런 | 강의

Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

[1] 시작 - JPQL vs Querydsl

 

▶ QMember, QTeam 등 Q클래스가 build 폴더에 없으면 gradle - other - compileQuerydsl을 실행하면 된다.

 

■ JPQL 테스트 코드

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

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

 

■ Querydsl

    @Test
    public void startQuerydsl() {

        Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1")) // 파라미터 바인딩 처리
                .fetchOne();

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

    }

① QMember.member를 static import함 (member)

② JPA QueryFactory queryFactory는 필드로 제공

동시성 문제는?

  • 동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EnityManger(em)에 달려있다.
  • 여러 쓰레드에서 동시에 같은 EntityManger에 접근하다고 해도 스프링 프레임워크에선 동시성 문제가 발생하지 않는다. 그 이유는 스프링이 트랜잭션마다 별도의 영속성 컨텍스트를 제공하기 때문이다.

 

 

■ JPQL vs Querydsl

  • Querydsl은 문자열을 사용하지 않고 자바 메서드 호출로 쿼리를 만든다.
    • JPQL은 문자열 쿼리로 만들어야 하므로 런타임 시점에야 오류를 확인할 수 있다.
    • 반면 Querydsl은 자바 메서드 호출로 쿼리를 만들기 때문에 컴파일 시점에 오류를 잡을 수 있다.
  • Qeurydsl은 JPQL처럼 파라미터 바인딩을 하지 않아도 eq, gt, got, goe 등의 메서드를 이용해 파라미터가 바인딩된 쿼리를 만들 수 있다.
    • 덕분에 SQL injection 공격도 잘 방어할 수 있다.


[2] 기본 Q-Type 활용

■ Q클래스 인스턴스를 사용하는 두 가지 방법

QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용

Q클래스가 만들어질 때 기본 인스턴스도 같이 생성된다. 여기서 별칭은 "member1"이다.

 

① 별칭 직접 지정

  • 별칭을 직접 지정하면 JPQL 별칭(Alias)도 m1으로 변한 것을 알 수 있다.

 

② 기본 인스턴스를 static import와 함께 사용

  • 기본 인스턴스는 static import로 처리해 member로 바꿈.

 

결론 : 같은 테이블을 조인해야 할 때(ex 서브쿼리)가 아니면 기본 인스턴스를 사용하자.


[3] 검색 조건 쿼리

JPQL이 제공하는 검색 조건 거의 대부분을 제공한다.

 

■ 기본 검색 쿼리

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

	assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • 검색조건에서 .and() .or()를 메서드 체인으로 연결할 수 있다.
  • select(member) .from(member)도 selectFrom(memebr)로 합칠 수 있다.

 

■ AND 조건을 파라미터로 처리

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

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
  • where()에 파라미터로 검색조건을 추가하면 AND가 자동으로 붙는다.
  • 이 경우 null값은 무시되는데 이를 통해 동적 쿼리를 깔끔하게 만들 수 있다. => 

 

■ Querydsl이 제공하는 검색 조건

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'

member.username.isNotNull() //이름이 is not null

member.age.in(10, 20) // age in (10,20)
member.age.notIn(10, 20) // age not in (10, 20)
member.age.between(10,30) //between 10, 30

member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30

member.username.like("member%") //like 검색
member.username.contains("member") // like ‘%member%’ 검색
member.username.startsWith("member") //like ‘member%’ 검색
...

 

[4] 결과 조회

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환(null 아니다)
  • fetchOne() : 단 건 조회
                    결과가 없으면 null
                    결과가 둘 이상이면 NonUniqueResultException 예외 발생
  • fetchFirst() : limit(1).fetchOne() 과 같다.
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행 (Deprecated, 향후 미지원)
  • fetchCount() :  count 쿼리로 변경하여 count 수 조회 (Deprecated, 향후 미지원)

 

■ 예시

    @Test
    public void resultFetch() throws Exception {
        List<Member> fetch = queryFactory
                .selectFrom(member)
                .fetch();

        Member fetchOne = queryFactory
                .selectFrom(member)
                .fetchOne();

        Member fetchFirst = queryFactory
                .selectFrom(member)
                .fetchFirst();

        QueryResults<Member> results = queryFactory
                .selectFrom(member)
                .fetchResults();

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


        long total = queryFactory
                .selectFrom(member)
                .fetchCount();
    }
영한님 曰
fetchResults·fetchCount() 기능은 select 구문을 단순히 count 처리하는 것으로 바꾸는 정도여서, 단순한 쿼리에서는 잘 동작하는데, 복잡한 쿼리에서는 잘 동작하지 않는다. 이럴 때는 명확하게 카운트 쿼리를 별도로 작성하고, 말씀하신 대로 fetch()를 사용해서 해결해야 한다.

출처 : 인프런 : groupby having절 사용 시 fetchCount(), fetchResults() 사용 가능 여부

 

[5] 정렬

  • desc() , asc() : 일반정렬[내림차순, 오름차순]
  • nullsLast() , nullsFirst() : null 데이터 순서 부여
    /**
     * 회원 정렬 순서
     * 1. 회원 나이 내림차순(desc)
     * 2. 회원 이름 올림차순(asc)
     * 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)     *
     */
    @Test
    public void sort() throws Exception {
        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(member6.getUsername()).isEqualTo("member6");
        assertThat(memberNull.getUsername()).isNull();
    }
member = Member(id=8, username=member5, age=100)
member = Member(id=9, username=member6, age=100)
member = Member(id=7, username=null, age=100)
  • 회원 이름이 없어서 id가 빠른데도 제일 마지막에 나온다.


[6] 페이징

■ 테스트 코드

    @Test
    public void paging() throws Exception {
        QueryResults<Member> queryResults = queryFactory.selectFrom(member)
                .orderBy(member.username.desc())
                .offset(1)
                .limit(2)
                .fetchResults();

        assertThat(queryResults.getTotal()).isEqualTo(4);
        assertThat(queryResults.getLimit()).isEqualTo(2);
        assertThat(queryResults.getOffset()).isEqualTo(1);
        assertThat(queryResults.getResults().size()).isEqualTo(2);

    }
  • offset은 0부터 시작 (어디서부터 가져올지)
  • limit 쿼리 결과에 대한 제한 (행을 얼마나 가져올지)

참고]

실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, 
count 쿼리는 조인이 필요 없는 경우도 있다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 
조인을 해버리기 때문에 성능이 안 나올 수 있다. count 쿼리에 조인이 필요 없는 성능 최적화가 필요하다면, 
count 전용 쿼리를 별도로 작성해야 한다.

 

[7] 집합

■ 집합함수

JPQL이 제공하는 모든 집합함수를 제공한다.

  • count() : 카운트
  • sum()   : 합
  • avg()    : 평균
  • max()   : 최대
  • min()    : 최소
    @Test
    public void aggregation() throws Exception {
        /**
         * JPQL
         * select
         *    COUNT(m),   //회원수
         *    SUM(m.age), //나이 합
         *    AVG(m.age), //평균 나이
         *    MAX(m.age), //최대 나이
         *    MIN(m.age)  //최소 나이
         * from Member m
         */
         
        //when
        List<Tuple> result = queryFactory.select(
                        member.count(),
                        member.age.sum(),
                        member.age.avg(),
                        member.age.max(),
                        member.age.min()
                )
                .from(member)
                .fetch();
                
        //then
        Tuple tuple = result.get(0);
        assertThat(tuple.get(member.count())).isEqualTo(4);
        assertThat(tuple.get(member.age.sum())).isEqualTo(100);
        assertThat(tuple.get(member.age.avg())).isEqualTo(25);
        assertThat(tuple.get(member.age.max())).isEqualTo(40);
        assertThat(tuple.get(member.age.min())).isEqualTo(10);

    }

 

■ GroupBy 사용

    /*
    팀의 이름과 각 팀의 평균 연령을 구하라.
     */
    @Test
    public void group() throws Exception {
        List<Tuple> result = queryFactory.select(team.name, member.age.avg())
                .from(member)
                .leftJoin(member.team, team)
                .groupBy(team.name)
                .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);

    }

 

having도 사용할 수 있다.

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