본문 바로가기
QueryDSL

Querydsl 기본 문법

by 이상한나라의개발자 2024. 3. 15.

QueryDsl은 SQL 쿼리를 코드 레벨에서 안전하고 직관적으로 작성할 수 있어, 개발자들 사이에서 인기가 높습니다. 요즘은 Springboot + Spring Data JPA + Querydsl은 기본으로 프로젝트를 진행할 정도죠. 아래는 다양한 QueryDsl 문법에 대해서 살펴 보도록하겠습니다.

 

기본 비교 연산자

QueryDsl은 일반적인 비교 연산자를 메서드 체인 형태로 제공합니다. 다음은 가장 많이 사용되는 비교 연산자들 입니다.

eq (equals)

"eq" 연산자는 값이 동일한지 비교할 때 사용됩니다. SQL의 "=" 과 동일합니다.

QMember qMember = QMember.Member;
qMember.username.eq("name");

 

ne (not equal)

"ne" 연산자는 값이 동일하지 않은지 비교할 때 사용됩니다. SQL의 "!=" 연산자와 동일합니다.

QMember qMember = QMember.Member;
qMember.username.ne("name");

 

lt (less than)

"lt" 연산자는 값이 첫 번째 값이 두번째 값보다 작은지 비교할 때 사용됩니다.  SQL의 "<" 과 동일합니다.

QMember qMember = QMember.Member;
qMember.age.lt(20)

 

le (less than or equal to)

"le" 연산자는 lt 연산자에서 동등비교를 붙인 연산자입니다. SQL의 "<=" 과 동일합니다.

QMember qMember = QMember.Member;
qMember.age.le(20);

 

gt (greater than)

"gt" 연산자는 첫 번째 값이 두 번째 값보다 큰지 비교할 때 사용됩니다. SQL의 ">" 과 동일합니다.

QMember qMember = QMember.Member;
qMember.age.gt(20);

 

ge (greater than or equal)

"ge" 연산자는 gt 연산자에서 동등 비교를 붙인 연산자입니다. SQL의 ">=" 과 동일합니다.

QMember qMember = QMember.Member;
qMember.age.ge(20);

 

논리 연산자

복잡한 조건을 표현하기 위해 "and", "or", "not" 같은 논리 연산자가 있습니다.

 

and 

"and" 연산자는 두 조건이 모두 참일 때, 참을 반환합니다. SQL의 "and" 와 동일합니다.

QMember qMember = QMember.Member;
qMember.age.ge(20).and(qMember.username.startsWith("name"));

 

or

"or" 연산자는 두 존건 중 하나라도 참일 때, 참을 반환합니다. SQL의 "or" 와 동일합니다.

QMember qMember = QMember.Member;
qMember.age.ge(20).or(qMember.username.endsWith("name"));

 

not

"not" 연산자는 조건의 결과를 반전시킵니다. SQL의 "not" 와 동일합니다.

QMember qMember = QMember.Member;
qMember.username.startWith("name").not();

 

위 예시의 결합 사용

QMember qMember = QMember.Member;
qMember.age.ge(20).and(qMember.age.lt(30)).and(qMember.username.startsWith("S").or(qMember.endsWith("A"))

 

나이가 20보다 크거나 같으면서 30보다 작은 회원 중에 이름이 S로 시작하거나 A로 끝나는 유저를 조회합니다.

 

기타 조건

기타 다양한 검색 조건을 제공합니다. 

 

isNull & isNotNull

"isNull" 은 필드 값이 null인 경우를 반환하고, isNotNull은 필드 값이 null이 아닌 경우를 반환합니다. SQL의 "is null, is not null" 과 동일합니다.

QMember qMember = QMember.Member;
qMember.username.isNull();
qMember.username.isNotNull();

 

like, startWith, endsWith, contains

문자열 필드에 대한 패턴 매칭을 위한 조건입니다. like는 SQL의 like 연산자와 유사하며, startsWith, endsWith, contains 는 각각 문자열이 특정 문자열로 시작하거나 끝나는지, 특정 문자열을 포함하는지 확인합니다.

QMember qMember = QMember.Member;
qMember.username.like("name%"); // name로 시작하는 모든 사용자를 조회 startsWith와 유사
qMember.username.like("%name"); // name로 끝나는 모든 사용자를 조회 endsWith와 유사
qMember.username.like("%name%"): // name을 포함하는 모든 사용자를 조회 contains와 유사
qMember.username.like("name"); // name이 정확히 일치하는 사용자 조회 eq와 유사

qMember.username.startsWith("name");
qMember.username.endsWith("name");
qMember.username.contains("name");
qMember.username.eq("name");

 

in & notIn

"in" 은 지정된 값의 목록 중 일치하는 경우를 조회합니다. "notIn"은 지정된 값과 일치하지 않는 경우를 조회합니다. SQL의 "in, not in"과 동일합니다.

QMember qMember = QMember.Member;
qMember.username.in("username1", "username2");
qMember.username.notIn("username1", "username2");

 

between

"between" 은 특정 범위 내의 값을 모두 조회 합니다. SQL의 "between a and b" 와 동일합니다.

QMember qMember = QMember.Member;
qMember.age.between(20,30);

 

 

QueryDsl은 이러한 다양한 조건을 통해 개발자가 데이터를 보다 정교하게 조회할수 있도록 지원합니다.

 

결과를 가져오는 Fetching 방식

QueryDsl에서 제공하는 결과를 가져오는 방식은 데이터베이스 쿼리의 결과를 다양한 형태로 처리할 수 있게 해줍니다. 이러한 방식들은 쿼리의 목적과 반환되어야 할 결과의 타입에 따라 선택하여 사용할 수 있습니다. 아래는 주요 결과 조회 방식입니다.

 

fetch()

"fetch()" 메서드는 쿼리 결과를 리스트로 반환합니다.

@PersistenceContext
private EntityManager em;

private JPQLQueryFactory queryFactory;

@BeforeEach
void init() {
    queryFactory = new JPAQueryFactory(em);
}

@Test
void fetch() {

    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.gt(10))
            .fetch();
}

 

fetchOne()

"fetchOne()" 메서드는 쿼리 결과를 단일 객체로 반환합니다. 이 방식은 하나만 존재할 것으로 기대될때 사용해야 합니다. 결과가 없거나 하나 이상일 경우는 예외를 발생합니다. 결과가 없으면 null, 결과가 둘 이상이면 NonUniqueResultException 이 발생된다. 

@Test
void fetchOne() {
    Member result = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"))
            .fetchOne();
}

 

fetchFirst()

"fetchFirst()" 는 limit(1).fetchOne() 의 축약형으로, 쿼리 결과 중 첫 번째만 반환합니다.

@Test
void fetchFirst() {

    Member result = queryFactory
            .selectFrom(member)
            .where(member.age.gt(10))
            .fetchFirst();
}

 

fetchResults() - deprecated

"fetchResults()" 메서드는 쿼리 결과를 QueryResults 객체로 변환합니다. 이 객체는 결과 리스트와 함께 결과 수(total count)를 포함 합니다. 주로 페이징 처리에 유용하게 사용됩니다.

@Test
void fetchResult() {

    QueryResults<Member> results = queryFactory
            .selectFrom(member)
            .where(member.age.gt(10))
            .fetchResults();
    List<Member> members = results.getResults();
    long total = results.getTotal();

    for (Member findMember : members) {
        System.out.println("findMember.getId() = " + findMember.getId());
        System.out.println("findMember.getUsername() = " + findMember.getUsername());
        System.out.println("findMember.getAge() = " + findMember.getAge());
    }
    System.out.println(total);
}

 

위 코드를 실행하면 total count를 가져오기 위한 쿼리, 실행 쿼리 두번 나가게 됩니다. 

 

만약, 데이터가 많거나 복잡한 쿼리의 경우는 total count 를 가져오면서 성능이 저하될 수 있기 때문에 total count 쿼리를 수정하여 별도의 쿼리로 수행하는 것이 좋습니다.

 

fetchCount()

"fetchCount()" 메서드는 쿼리에 해당하는 결과의 총 개수만을 반환합니다. 결과의 내용 데이터 수량이 필요할 때 사용됩니다.

@Test
void 테스트() {

    long count = queryFactory
            .selectFrom(member)
            .fetchCount();
            
    System.out.println("count = " + count);
}

 

다양한 fetching 방식을 통해 개발자가 필요에 따라 최적의 쿼리 실행 방법을 선택할 수 있습니다.

 

정렬

QueryDsl에서 정렬(sorting)은 쿼리 결과를 특정 기준에 따라 순서대로 나열하는 기능입니다. 이 기능은 orderBy 메소드를 사용하여 구현할 수 있으며, asc(), desc() 메소드를 통해 오름차순 또는 내림차순 정렬을 지정할 수 있습니다.

오름차순, 내림차순으로 정렬하기 

@Test
void 회원의_나이를_오름차순으로_정렬한다() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.age.asc())
            .fetch();
}

@Test
void 회원의_나이를_내림차순으로_정렬한다() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.age.desc())
            .fetch();
}

@Test
void 회원의_나이_내림차순_이름_오름차순으로_정렬하고_회원_이름이없으면_null먼저_출력한다() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.age.asc(), member.username.desc().nullsFirst())
            .fetch();
}

 

페이징

QueryDsl을 사용한 페이징 처리는 데이터베이스로 부터 데이터를 페이지 단위로 효율적으로 가져오는 방법입니다. 복잡한 쿼리의 경우, 전체 데이터 수 (total count)를 가져오는 쿼리와 실제 데이터를 가져오는 쿼리를 분리하여 성능을 최적화할 수 있습니다. 아래에서는 QueryDsl의 페이징 처리 방식 두 가지를 설명하겠습니다.

 

fetchResults를 사용한 페이징 - deprecated

QueryDsl의 fetchResults의 경우 count 쿼리를 같이 만들어주는 기능이 있습니다. 하지만, 복잡한 쿼리의 경우 쿼리가 잘 동작하지 않고 예외가 발생하는 등 문제점이 발행하여 향후 지원을 하지 않는다고 합니다. 그러므로, 간단하게 알아 보도록 하겠습니다.

 

@Test
void fetchResults_를_활용한_페이징_쿼리() {

    QueryResults<Member> queryResults = queryFactory
            .selectFrom(member)
            .where(member.age.gt(10))
            .orderBy(member.age.desc())
            .offset(0) // 페이지 번호
            .limit(2)  // 페이지당 데이터 수
            .fetchResults();

    List<Member> members = queryResults.getResults(); // 실제 조회한 데이터
	
    // 페이지 번호, 페이지당 데이터 수, total count 정보는 QueryResults에 있음.
    System.out.println("queryResults.getLimit() = " + queryResults.getLimit());
    System.out.println("queryResults.getOffset() = " + queryResults.getOffset());
    System.out.println("queryResults.getTotal() = " + queryResults.getTotal());
    System.out.println("queryResults.getResults() = " + queryResults.getResults());
    System.out.println("members = " + members);
}

 

fetchResults() 메소드는 페이징 처리에 필요한 결과 리스트와 전체 결과 수를 함께 반환합니다. 하지만, 결과 데이터와 전체 데이터 수를 계산하기 위해 같은 조건의 total count, fetch 두번의 쿼리를 내부적으로 실행합니다. 복잡한 쿼리에서는 total count 쿼리의 튜닝을 하여 개선할 수 있는데요 fetchResults()를 사용하게 되면 튜닝이 어려워져 성능상 이점이 사라집니다.

 

fetch 쿼리와 totalCount 쿼리를 분리한 페이징 - 권장

성능을 고려하여, 실제 데이터를 가져오는 쿼리와 전체 데이터 수를 가져오는 쿼리를 분리할 수 있습니다. 이 방식은 특히 전체 데이터 수를 계산하는 쿼리가 복잡하고 비용이 많이 드는 경우 성능상의 이점을 제공합니다. 실제 데이터 수 조회에는 offset(), limit()를 사용하고 전체 데이터 수 조회에는 간소화된 쿼리를 사용합니다. 또한, 실제 데이터를 가져오는 쿼리에는 orderBy를 사용하지만 전체 데이터 수를 가져오는 쿼리에는 사용하지 않습니다.

@Test
void fetch_쿼리와_totalCount_쿼리_분리() {
    List<Member> members = queryFactory
            .selectFrom(member)
            .where(member.age.gt(10))
            .orderBy(member.age.desc())
            .offset(0)
            .limit(2)
            .fetch();

    long count = queryFactory
            .selectFrom(member)
            .where(member.age.gt(10))
            .fetchCount();

    assertThat(members.size()).isEqualTo(2);
    assertThat(count).isEqualTo(3);
}

 

집합 함수

QueryDsl의 집합 함수는 데이터의 집합에 대한 계산을 수행하고 결과를 반환하는 함수입니다. 주로 그룹화된 쿼리의 결과에 대한 요약 정보를 제공하는데 사용됩니다. QueryDsl에서 지원하는 주요 집합 함수와 그 사용 방법을 알아봅니다.

 

집합 함수의 종류

  • COUNT : 그룹 내 항목의 수를 구합니다.
  • SUM : 숫자로 이루어진 그룹의 총합을 계산합니다.
  • AVG : 숫자로 이루어진 그룹의 평균 값을 계산합니다.
  • MAX : 그룹 내 최대값을 찾습니다.
  • MIN : 그룹내 최소값을 찾습니다.
  • GROUP BY, HAVING : SQL에서 데이터를 그룹화하고, 특정 조건을 만족하는 그룹에 대한 쿼리를 수행할 때 사용되는 구문입니다. GROUP BY는 선택한 컬럼의 값이 같은 행들을 그룹화 하는데 사용되며, HAVING는  GROUP BY를 통해 생성된 그룹에 조건을 적용할 때 사용 됩니다. 주로 집계 함수 (COUNT, SUM, AVG, MAX, MN)와 함께 사용 됩니다.

사용 방법 및 예시

@Test
void 집합_함수_COUNT_사용() {

    Long memberCount = queryFactory
            .select(member.count())
            .from(member)
            .where(member.age.gt(20))
            .fetchOne();

    assertThat(memberCount).isEqualTo(2);
}

@Test
void 집합_함수_AVG_사용() {

    Double memberAvg = queryFactory
            .select(member.age.avg())
            .from(member)
            .fetchOne();

    assertThat(memberAvg).isEqualTo(25.0);
}

@Test
void 집합_함수_SUM_사용() {

    Integer memberAvgSum = queryFactory
            .select(member.age.sum())
            .from(member)
            .fetchOne();

    assertThat(memberAvgSum).isEqualTo(100);
}

@Test
void 집합_함수_MAX_사용() {

    Integer memberAgeMax = queryFactory
            .select(member.age.max())
            .from(member)
            .fetchOne();

    assertThat(memberAgeMax).isEqualTo(40);
}

@Test
void 집합_함수_MIN_사용() {

    Integer memberAgeMin = queryFactory
            .select(member.age.min())
            .from(member)
            .fetchOne();

    assertThat(memberAgeMin).isEqualTo(10);
}

@Test
void 집합_함수_사용() {

    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.0);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
}

 

GROUP BY & HAVING 사용 방법 및 예시

Member 와 Team 엔티티를 사용하여 예시를 만들어보겠습니다. 이 예시에서는 각 팀별로 멤버 수를 세고, 멤버 수가 특정 기준 이상인 팀만 조회하는 쿼리 입니다.

 

@Test
void 각_팀별로_수를_세고_멤버_수가_1명_이상인_팀만_조회() {

    List<Tuple> result = queryFactory
            .select(team.name, member.count())
            .from(member)
            .join(member.team, team)
            .groupBy(team.name)
            .having(member.count().goe(2))
            .fetch();

    Tuple tuple = result.get(0);
    Tuple tuple1 = result.get(1);

    assertThat(tuple.get(team.name)).isEqualTo("teamA");
    assertThat(tuple.get(member.count())).isEqualTo(2);

    assertThat(tuple1.get(team.name)).isEqualTo("teamB");
    assertThat(tuple1.get(member.count())).isEqualTo(2);

}

 

CASE

QueryDsl의 cas문은 sql의 case 구문과 유사한 조건부 로직을 java 코드 내에서 구현할 수 있게 해줍니다. QueryDsl에서 CaseBuilder를 사용하여 case 문을 구현할 수 있습니다. 이는 다양한 조건을 체크하고 각 조건에 따른 값을 반환할 수 있게 해줍니다.

 

아래 코드는 학생의 점수로 등급을 구분하는 예시입니다.

@Test
void CASE_문_테스트() {

    List<Tuple> result = queryFactory
            .select(
                    student.studentName,
                    new CaseBuilder()
                            .when(student.score.between(90, 100)).then("A")
                            .when(student.score.between(30, 50)).then("C")
                            .when(student.score.between(0, 20)).then("D")
                            .otherwise("F").as("GRADE")
            )
            .from(student)
            .fetch();

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

 

문자열 더하기

QueryDsl 에서 문자열을 더하는 경우, concat 함수를 사용합니다. concat 함수는 두 개 이상의 문자열을 서로 연결할 때 사용됩니다. 이 방법은 주로 엔티티 필드 값을 결합할 때 유용하며, 쿼리 결과의 일부로 반환되는 문자열을 동적으로 생성할 때 사용됩니다.

 

아래 코드는 두개의 문자열을 더하면서 case 구문 까지 추가한 예시 입니다.

@Test
void 문자_더하기() {

    List<Tuple> result = queryFactory
            .select(student.studentName.as("name"),
                    student.score.stringValue().concat("_").concat(
                            new CaseBuilder()
                                    .when(student.score.between(90, 100)).then("A")
                                    .when(student.score.between(30, 50)).then("C")
                                    .when(student.score.between(0, 20)).then("D")
                                    .otherwise("F")
                    ).as("info")
            )
            .from(student)
            .fetch();
}

'QueryDSL' 카테고리의 다른 글

QueryDsl 프로젝션 결과 반환  (0) 2024.03.25
QueryDsl 서브쿼리  (0) 2024.03.20
QueryDsl 조인  (0) 2024.03.20
QueryDsl 소개  (0) 2024.03.15
Springboot3.x에서 Querydls 설정  (0) 2024.03.14