FireDrago

[QueryDSL] 기본 문법 1 본문

프로그래밍/QueryDSL

[QueryDSL] 기본 문법 1

화이용 2024. 5. 1. 14:19

QueryDSL은 코드 기반의 쿼리 작성 도구로, 문자열 기반 JPQL 대신 자바 코드를 통해 쿼리를 작성한다.

이를 통해 다음과 같은 장점을 제공한다.

 

1. 코드 가독성 향상

가장 큰 장점은 코드 가독성 향상이다. 문자열 기반 쿼리는 이해하기 어려울 수 있지만,

QueryDSL은 자연스러운 자바 코드로 쿼리를 작성할 수 있어 코드를 읽고 이해하기 쉽다.

특히 복잡한 쿼리일수록 이 효과가 더욱 두드러진다.

 

2. 컴파일 시점 오류 검증

QueryDSL은 컴파일 시점에 쿼리 문법 오류를 검증한다. 런타임 오류 발생 가능성을 줄여 코드 안정성을 높일 수 있다.

또한, IDE의 자동 완성 기능을 활용하여 쿼리 작성 속도를 높일 수 있다.

 

3. 재사용 가능한 쿼리 로직

조건절이나 정렬 기준 등을 메서드 형태로 작성하여 쿼리 로직을 재사용할 수 있다.

코드 중복을 줄이고 유지 관리 편의성을 높인다.

 

4. 안전한 데이터 접근

QueryDSL은 SQL 인젝션 공격으로부터 보호하는 기능을 제공한다.

setParameter() 메서드를 사용하지 않아도 내부에서 파라미터를 사용한다.

 

 

검색조건 쿼리

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

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

@Test
void searchAndParam() {
    Member findMember = queryFactory
            .selectFrom(member)
            .where( // and의 경우 파라미터로 넘길수도 있다.
                    member.username.eq("member1"),
                    member.age.eq(10)
            )
            .fetchOne();

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

 

and 조건의 경우 .and() 를 사용하거나 where 절에서 파라미터로 넘길수 있다. 

JPQL이 제공하는 모든 검색조건을 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%’ 검색

 

 

결과조회

결과조회 메서드 설명
fetch() 리스트 조회, 데이터가 없다면 빈 리스트 반환
fetchOne() 단 건조회, 결과 없으면 null,
결과가 둘 이상이면 com.querydsl.core.NonUniqueResultException
fetchFirst() limit(1).fetchOne() 최초값 단건조회
fetchResults() 페이징 정보 포함, total count 쿼리 추가실행
fetchCount() count 쿼리로 변경해서 count 수 조회

 

@Test
void resultFetch() {
    // List 조회 -> fetch()
    List<Member> fetch = queryFactory
            .selectFrom(member)
            .fetch();

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

    // 처음 한 건 조회
    Member findMember2 = queryFactory
            .selectFrom(member)
            .fetchFirst();

    // 페이징에서 사용
    QueryResults<Member> results = queryFactory
            .selectFrom(member)
            .fetchResults();

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

    // count 쿼리로 변경
    long count = queryFactory.selectFrom(member)
            .fetchCount();
}

 

 

정렬

desc() , asc() : 일반 정렬

nullsLast(), nullsFirst() : null 데이터 순서 부여

/**
 * 회원 정렬 순서
 * 1. 회원 나이 내침차순 (desc)
 * 2. 회원 이름 올림차순 (asc)
 * 단 2에서 회원 이름이 없으면 마지막에 출력 (nulls last)
 */
@Test
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(member6.getUsername()).isEqualTo("member6");
    assertThat(memberNull.getUsername()).isNull();
}

 

 

페이징

// **목적:** 단순 데이터 조회 테스트 (페이징 정보 없음)
// **조건:** 
//  - member 테이블에서 모든 데이터를 username 기준 내림차순으로 조회
//  - offset 1, limit 2를 적용하여 2번째 데이터부터 2개의 데이터만 가져옴
@Test
void paging1() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1)
            .limit(2)
            .fetch();

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

// **목적:** 페이징 정보 포함 조회 테스트
// **조건:** 
//  - member 테이블에서 모든 데이터를 username 기준 내림차순으로 조회
//  - offset 1, limit 2를 적용하여 2번째 데이터부터 2개의 데이터만 가져옴
//  - QueryResults 객체를 사용하여 페이징 정보 (전체 조회 수, 페이지당 데이터 수, 시작 위치, 결과 목록)를 얻음
@Test
void paging2() {
    QueryResults<Member> queryResult = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1)
            .limit(2)
            .fetchResults();

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

 

페이징 쿼리를 작성할때 데이터 조회 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요없는 경우가 있다.

그런데 fetchResult() 를 사용하면 count 쿼리역시 모두 조인을 하기 때문에 성능이 안나올 수 있다.

count 쿼리에 조인이 필요없는 성능최적화가 필요하다면, count 전용쿼리를 별도로 작성해야 한다.

 

 

집합

/**
 * JPQL
 * select
 * COUNT(m), //회원수
 * SUM(m.age), //나이 합
 * AVG(m.age), //평균 나이
 * MAX(m.age), //최대 나이
 * MIN(m.age) //최소 나이
 * from Member m
 */
@Test
public void aggregation() throws Exception {
    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.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);
}

/**
 * 팀의 이름과 각 팀의 평균 연령을 구하라
 */
@Test
void group() 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("teamA");
    assertThat(teamA.get(member.age.avg())).isEqualTo(15);

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

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

Group By 사용할때 그룹화된 결과를 제한하려면 having() 을 사용하면 된다.

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

 

'프로그래밍 > QueryDSL' 카테고리의 다른 글

[QueryDSL] QueryDSL 사용법  (0) 2024.03.12