5 분 소요

📌 Topic

  • JPQL vs Querydsl
  • 기본 Q-Type 활용
  • 검색 조건 쿼리
  • 결과 조회
  • 정렬
  • 페이징
  • 집합

01. JPQL vs Querydsl

이번 시간에는 JPQL과 Querydsl의 차이에 대해 알아보는 시간을 갖는다

01-1. QueryDslBasicTest 테스트 작성

// JPQL 사용
@Test
@DisplayName("단일 회원 검색 - JPQL 사용")
public void findMemberByUserNameWithJPQL() {
    //member1 을 찾아라.
    String qlString =
            "select m from Member m " +
            "where m.userName = :userName";

    Member findMember = em.createQuery(qlString, Member.class)
                            .setParameter("userName", "김영민")
                            .getSingleResult();

    assertThat(findMember.getUserName()).isEqualTo("김영민");
}
  • JPQL은 쿼리를 직접 작성할 수 있다
  • 하지만 JPQL은 문자열이기 때문에 컴파일 시점에 문제를 잡을 수 없다
  • 또한 파라미터 바인딩도 위와 같이 직접 선언을 해주어야 한다
// Querydsl 사용
@Test
@DisplayName("단일 회원 검색 - Querydsl 사용")
public void findMemberByUserNameWithQuerydsl() {
    // 01. JPAQueryFactory 생성
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);
    QMember m = new QMember("m");

    Member findMember = queryFactory
            .select(m)
            .from(m)
            .where(m.userName.eq("김영민"))
            .fetchOne();

    assertThat(findMember.getUserName()).isEqualTo("김영민");
}
  • JPAQueryFactory를 만들 때 EntityManager 객체를 생성자에 전달
    • QMember는 변수명에다 별칭(alias)를 주는게 좋다, 안써도 상관 없음
    • 나머지 부분은 일반적인 SQL의 형태와 동일하다
  • JPQL은 문자로 작성, Querydsl은 자바 코드로 작성이 되는 장점이 있다
  • 즉, 자바 컴파일 시점에 요류를 잡아낼 수 있다
  • 파라미터 바인딩을 자동으로 수행 해준다
@SpringBootTest
@Transactional
@DisplayName("JPQL <> Querydsl 테스트")
public class QueryDslBasicTest {

    @PersistenceContext
    EntityManager em;

    JPAQueryFactory queryFactory;

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

        //..
    }

    //...
}
  • JPAQueryFactory는 맴버 변수로 빼서 사용이 가능하다
  • 동시성 문제가 발생하지 않도록 내부 설계가 구현 되어 있음
  • @BeforeEach가 붙어있는 before 메서드에서 초기화를 진행함

02. 기본 Q-Type 활용

이번 시간에는 기본 Q클래스 인스턴스를 이용하는 3가지 방법을 알아보자

기본 Q클래스를 사용하는 방법은 아래와 같이 3가지가 존재한다.

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

02-1. Q Class 확인을 위한 테스트 코드 작성

@Test
@DisplayName("Querydsl에서 Q Class를 사용하는 방법 테스트")
public void createQuerydslQueueClass() {
    // 기본 인스턴스 사용
    QMember m1 = member;

    // QMember.member -> member ==> static import
    Member findMember = queryFactory
            .select(member)
            .from(member)
            .where(member.userName.eq("김영민"))
            .fetchOne();

    assertThat(findMember.getUserName()).isEqualTo("김영민");
}
  • 위에서는 static import를 통해 QMember 사용
  • 내부적으로 들어가보면 static final로 선언이 되어있음
  • Querydsl은 결국 JPQL의 빌더(Builder) 역할을 수행한다
    • 즉, Querydsl로 작성한 코드는 JPQL이 된다는 의미다

03. 검색 조건 쿼리

이번 시간에는 Querydsl의 where절에 사용되는 검색 조건에 대해 알아보자

03-1. 테스트 코드 작성

@Test
@DisplayName("Querydsl의 where절에 사용되는 구문 테스트")
public void search() throws Exception {
    // 이름이 임수현이 아닌 모든 회원 조회
    List<Member> findMember_1 = queryFactory
            .selectFrom(member)
            .where(member.userName.ne("임수현"))
            .fetch(); // ne

    assertThat(findMember_1).hasSizeGreaterThan(0);

    // 이름이 임수현이고, 나이가 29인 단일 회원 조회
    Member findMember_2 = queryFactory
            .selectFrom(member)
            .where(member.userName.eq("임수현")
                                  .and(member.age.eq(29)))
            .fetchOne(); // eq

    assertThat(findMember_2.getUserName()).isEqualTo("임수현");

    // 나이가 20 ~ 29 사이에 존재하는 회원 조회
    List<Member> findMember_3 = queryFactory
            .select(member)
            .from(member)
            .where(member.age.between(20, 29))
            .fetch();

    assertThat(findMember_3).isNotEmpty();
    assertThat(findMember_3).hasSize(3); // expect) 3명의 회원

    for (Member member : findMember_3) {
        System.out.println(">>>>>>>> member.getUserName = " + member.getUserName() + ", age = " + member.getAge());
    }
}

이전과 달라진 부분은 from절이 select절과 합쳐지고
where절에 and 조건이 추가되었다.

03-2. JPQL이 제공하는 모든 검색 조건 제공

member.userName.eq("김철수") // userName == '김철수'
member.userName.ne("김철수") // userName != '김철수'
member.userName.eq("김철수").not() // userName != '김철수'

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)

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("김철%") // like 검색
member.userName.contains("member") // like '%member%' 검색
member.userName.startsWith("member") // like 'member%' 검색
  • Querydsl에서 제공하는 대표적인 조건
  • 위에서 언급하였지만, JPQL에서 지원되는 모든 부분을 지원한다

03-3. 검색 조건 AND를 다른 방식으로 사용

// AS-IS
@Test
@DisplayName("Querydsl의 where절에 사용되는 구문 테스트 - 기존 and 사용 방식")
public void searchAndParam() throws Exception {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.userName.eq("김영민").and(member.age.eq(30))) // 기존 and 사용 방식
            .fetchOne();

    assertThat(findMember.getUserName()).isEqualTo("김영민");
    assertThat(findMember.getAge()).isEqualTo(30);
}
// TO-BE
@Test
@DisplayName("Querydsl의 where절에 사용되는 구문 테스트 - and를 다른 방식으로 사용")
public void searchAndParam() throws Exception {
    Member findMember = queryFactory
            .selectFrom(member)
            .where(
                    member.userName.eq("김영민"), // 변경된 and 사용 방식
                    member.age.eq(30)
            )
            .fetchOne();

    assertThat(findMember.getUserName()).isEqualTo("김영민");
    assertThat(findMember.getAge()).isEqualTo(30);
}
  • and, or는 위와 같이 ‘,’로 구분하여 사용이 가능하다
  • 동적 쿼리 사용시에 유용하게 활용이 가능한 부분이다

04. 결과 조회

이번 시간에는 Querydsl의 결과 조회 메서드에 대해 정리 해보자

  • fetch()
    • 리스트 조회, 데이터 없을 시 빈 리트스 반환
    • 빈 리스트를 반환하기에 불필요한 IF ~ ELSE 사용 피할 수 있음
  • fetchOne()
    • 단 건 조회
    • 결과가 없으면 null
    • 결과가 둘 이상이면 com.querydsl.core.NoUniqueResultException
  • fetchFirst()
    • limit(1).fetchOne()
  • fetchResults()
    • 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount()
    • count 쿼리로 변경해서 count 수 조회

04-1. 결과 조회 테스트 코드 작성

@Test
@DisplayName("결과 조회 테스트")
public void resultFetchTest() throws Exception {
    /*List<Member> memberList = queryFactory
            .selectFrom(member)
            .fetch();

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

    Member firstMember = queryFactory
            .selectFrom(member)
            .fetchFirst();*/

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

    result.getTotal(); // count query

    List<Member> results = result.getResults(); // contents query

    long total = queryFactory
                .selectFrom(member)
                .fetchCount();
}

fetchResults 같은 경우에 페이징 쿼리가 복잡해지면
컨텐츠 쿼리와 total 쿼리가 다른 경우가 존재한다.

만약 복잡한 페이징 처리가 필요하다면
컨텐츠 쿼리와 total 쿼리를 분리하여 2번의 쿼리를 날리는 것이 좋다.

  • fetch query
  • fetchOne query
  • fetchFirst query
  • fetchResults query

05. 정렬

@Test
@DisplayName("정렬 테스트 진행")
public void test() throws Exception {
    // 회원 이름 순으로 내림차순, 나이 순으로 오름차순
    em.persist(new Member(null, 100));
    em.persist(new Member("김영민", 100));
    em.persist(new Member("정주리", 100));

    //querydsl 작성
    List<Member> memberList = queryFactory
            .selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(member.age.asc(), member.userName.desc().nullsLast())
            .fetch();

    System.out.println(">>>> memberList = " + memberList.toString());

    Member member = memberList.get(0);
    Member member1 = memberList.get(1);
    Member member2 = memberList.get(2);
    assertThat(member.getUserName()).isEqualTo("정주리");
    assertThat(member1.getUserName()).isEqualTo("김영민");
    assertThat(member2.getUserName()).isNull();
}

정렬의 경우 기존 SQL와 동일하게 orderBy 메서드를 사용하여 정렬 가능.

06. 페이징

@Test
@DisplayName("일반 페이징 테스트 진행")
public void paging() throws Exception {
    // Team 3개, Member 5명
    // Querydsl의 offset은 0부터 시작이 된다
    // offset: 시작 인덱스(row)
    // limit: 조회 개수
    List<Member> memberListByPaging = queryFactory // 김영민, 원영식, 임수현, 박진우, 김진엽
            .selectFrom(member)
            .orderBy(member.age.desc())
            .offset(0)
            .limit(5)
            .fetch();
    System.out.println(">>>> memberListByPaging = " + memberListByPaging.toString());

    assertThat(memberListByPaging.size()).isEqualTo(5);
}
@Test
@DisplayName("Count 포함 페이징 테스트 진행")
public void pagingWithCount() throws Exception {
    QueryResults<Member> queryResult = queryFactory
            .selectFrom(member)
            .orderBy(member.age.desc())
            .offset(0)
            .limit(10)
            .fetchResults();

    assertThat(queryResult.getResults().size()).isEqualTo(5); // 전체 회원 사이즈
    assertThat(queryResult.getLimit()).isEqualTo(10);
    assertThat(queryResult.getTotal()).isEqualTo(5);
    assertThat(queryResult.getOffset()).isEqualTo(0);
}

Querydsl은 offset, limit을 통해 페이징을 지원한다

  • offset
    • 페이지 인덱스(Row) 지정
  • limit
    • 가져올 데이터 개수 지정
select
    member0_.member_id as member_i1_1_,
    member0_.age as age2_1_,
    member0_.team_id as team_id4_1_,
    member0_.user_name as user_nam3_1_
from
    member member0_
order by
    member0_.user_name desc limit ? offset ?

실제로 DB에 요청된 쿼리는 위와 같다

07. 집합

@Test
@DisplayName("집합 테스트")
public void aggregation() throws Exception {
    // Querydsl Tuple 타입
    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(5);
    assertThat(tuple.get(member.age.sum())).isEqualTo(147);
    assertThat(tuple.get(member.age.avg())).isEqualTo(29.4);
    assertThat(tuple.get(member.age.max())).isEqualTo(33);
    assertThat(tuple.get(member.age.min())).isEqualTo(27);
}

Tuple을 사용하는 이유는 다양한 타입을 받아오기 때문이다.
하지만 실무에서는 DTO를 통해 SELECT절의 값을 받아온다.

select
    count(member0_.member_id) as col_0_0_,
    sum(member0_.age) as col_1_0_,
    avg(cast(member0_.age as double)) as col_2_0_,
    max(member0_.age) as col_3_0_,
    min(member0_.age) as col_4_0_
from
    member member0_

위 테스트코드가 실행된 쿼리는 위와 같다.

@Test
@DisplayName("GroupBy 테스트")
public void groupBy() throws Exception {
    // 팀의 이름과 각 팀의 평균 연령을 구해라.

    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .fetch();

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

    assertThat(teamA.get(team.name)).isEqualTo("데이터 플랫폼 팀");
    assertThat(teamA.get(member.age.avg())).isEqualTo(31.5);

    assertThat(teamB.get(team.name)).isEqualTo("인프라 팀");
    assertThat(teamB.get(member.age.avg())).isEqualTo(28);
}

GroupBy 테스트 코드

참고 자료

댓글남기기