Search

[강의 요약] 김영한 - QueryDSL

Last update: @5/24/2023
주의
본 포스팅은 인프런 강의를 통해 학습한 내용을 임의로 요약한 것으로 일부 내용의 오류 및 누락, 링크 숨김 등이 존재합니다.

설정 (스프링 부트 2.6 이상, Querydsl 5.0)

build.gradle에 다음 추가
buildscript { ext { queryDslVersion = "5.0.0" } } ... plugins { ... id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" } dependencies { ... implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}" ... } ... def querydslDir = "$buildDir/generated/querydsl" querydsl { jpa = true querydslSourcesDir = querydslDir } sourceSets { main.java.srcDir querydslDir } configurations { querydsl.extendsFrom compileClasspath } compileQuerydsl { options.annotationProcessorPath = configurations.querydsl }
Java
복사
Q타입 생성
Q타입은 QueryDSL로 쿼리를 생성할 때 엔티티의 정보를 제공하는 역할을 함
Gradle → Tasks → build → clean
Gradle → Tasks → other → compileQuerydsl
또는 터미널에서 프로젝트 디렉터리로 이동 후
./gradlew clean compileQuerydsl
build → generaged → querydsl 디렉터리에 Q타입 클래스 생성 확인
QueryDSL이 생성하는 JPQL 보기
application 설정 파일에 아래 추가
spring.jpa.properties.hibernate.use_sql_comments: true
Java
복사

QueryFactory 등록

QueryDSL을 사용하기 위해 QueryFactory 등록 필요
빈으로 등록하기
@Bean JPAQueryFactory jpaQueryFactory(EntityManager em) { return new JPAQueryFactory(em); }
Java
복사
동시성 문제는 EntityManager에게 달렸는데, 스프링 빈으로 등록되는 EntityManager는 트랜잭션마다 별도의 영속성 컨텍스트를 제공하는 라우터 역할을 하기 때문에 동시성 문제를 걱정하지 않아도 됨

Q 클래스 인스턴스 사용하기

QMember qMember = new QMember("m"); //별칭 직접 지정 QMember qMember = QMember.member; //기본 인스턴스 사용
Java
복사
같은 테이블을 조인해야 하는 경우가 아니면 기본 인스턴스를 사용
static import를 해두면 편함
import static study.querydsl.entity.QMember.*;
Java
복사
 아래 나오는 member 등의 엔티티는 이 QMember.memer처럼 기본 엔티티를 의미한디

기본 사용법 - 단건 조회

단건 조회
Member findMember = queryFactory .select(member) .from(member) .where(member.username.eq("member1")) .fetchOne();
Java
복사

Where절 검색 조건

where절에 들어가는 검색 조건
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, 30member.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%’ 검색 ...
Java
복사
where절 and 조건 파라미터 처리
Member foundMember = queryFactory .selectFrom(member) .where( member.username.eq("member1"), member.age.eq(10) ) .fetchOne();
Java
복사
Null값이 들어오면 무시됨(동적 쿼리에 활용)

결과 조회 및 total count 쿼리

fetch()
리스트 조회. 없으면 빈 리스트 반환
fetchOne()
단 건 조회
결과가 없으면 null
결과과 둘 이상이면 com.querydsl.core.NonUniqueResultException 발생
fetchFirst()
limit(1).fetchOne()과 같음
fetchResults() → deprecated
페이징 정보 포함(total count 쿼리 추가 실행)
fetchCount() → deprecated
count 쿼리로 변경해서 count 조회
count 쿼리가 필요하면 아래처럼 별도로 조회
Long totalCount = queryFactory .select(member.count()) .from(member) .fetchOne();
Java
복사
또는 PageableExecutionUtils.getPage() 활용해서 Page 객체로 반환
import org.springframework.data.support.PageableExecutionUtils; //패키지 변경 public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) { List<MemberTeamDto> content = queryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.username, member.age, team.id.as("teamId"), team.name.as("teamName"))) .from(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername())) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); // 별도 count query JPAQuery<Long> countQuery = queryFactory .select(member.count()) .from(member) .leftJoin(member.team, team) .where(usernameEq(condition.getUsername())); return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); }
Java
복사
이렇게 할 경우 count 쿼리가 나갈 필요가 없으면 날리지 않음
첫 페이지면서 조회 데이터 개수가 size보다 작을 때 (데이터 개수 = total count)
마지막 페이지일 때 (offset + 데이터 개수 = total count)

정렬

List<Member> result = queryFactory .selectFrom(member) .where(member.age.eq(100)) .orderBy(member.age.desc(), member.username.asc().nullsLast()) .fetch();
Java
복사
desc() , asc() : 일반 정렬
nullsLast() , nullsFirst() : null 데이터 순서 부여

페이징 (offset, limit)

QueryResults<Member> queryResults = queryFactory .selectFrom(member) .orderBy(member.username.desc()) .offset(1) // 0부터 시작 .limit(2) .fetchResults();
Java
복사

집계 함수

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); assertEquals(4, tuple.get(member.count())); assertEquals(100, tuple.get(member.age.sum())); assertEquals(25, tuple.get(member.age.avg())); assertEquals(40, tuple.get(member.age.max())); assertEquals(10, tuple.get(member.age.min()));
Java
복사
tuple은 Querydsl에서 제공하는 특수한 자료구조이기 때문에 가급적 리포지토리 계층에서만 사용
tuple은 엔티티나 DTO 조회가 아닌 경우에 반환됨
groupby()
List<Tuple> result = queryFactory .select(team.name, member.age.avg()) .from(member) .join(member.team, team) .groupBy(team.name) .fetch();
Java
복사
having()
... .groupBy(item.price) .having(item.price.gt(1000)) ...
Java
복사

조인

join()
기본적으로 id로 조인 (inner join)
List<Member> result = queryFactory .selectFrom(member) .join(member.team, team) // 조인 대상, 별칭 순서 .where(team.name.eq("teamA")) .fetch();
Java
복사
id 조인 외에 조인 조건을 추가하려면 on() 추가 (left outer join)
List<Tuple> result = queryFactory .select(member, team) .from(member) .leftJoin(member.team, team) .on(team.name.eq("teamA")) .fetch();
Java
복사
inner join이라면 굳이 on절을 추가하지 않고 where절을 활용 하는 것이 나음
theta join
연관관계가 없는 엔티티끼리 조인
List<Member> result = queryFactory .select(member) .from(member, team) .where(member.username.eq(team.name)) .fetch();
Java
복사
페치 조인
지연로딩 설정 시 N+1 문제 해결
Member foundMember = queryFactory .selectFrom(member) .join(member.team, team).fetchJoin() .where(member.username.eq("member1")) .fetchOne();
Java
복사

서브쿼리

where절에 사용
List<Member> result = queryFactory .selectFrom(member) .where(member.age.eq( select(memberSub.age.max()) .from(memberSub) )) .fetch();
Java
복사
List<Member> result = queryFactory .selectFrom(member) .where(member.age.goe( select(memberSub.age.avg()) .from(memberSub) )) .fetch();
Java
복사
List<Member> result = queryFactory .selectFrom(member) .where(member.age.in( select(memberSub.age) .from(memberSub) .where(memberSub.age.gt(10)) )) .fetch();
Java
복사
select절에 사용 (하이버네이트 구현체 사용 시)
List<Tuple> result = queryFactory .select(member.username, select(memberSub.age.avg()) .from(memberSub)) .from(member) .fetch();
Java
복사
from절 서브쿼리(인라인 뷰)는 지원하지 않음 (JPA, 하이버네이트 미지원)
서브쿼리를 join으로 변경하거나 쿼리를 분리해서 실행
또는 native SQL 사용
특히 어드민 페이지처럼 성능이 중요하지 않으면 애플리케이션 레벨에서 순차적으로 로직을 풀어가는 것이 좋음

Case문

select, where, orderBy에서 사용 가능
List<String> result = queryFactory .select(member.age .when(10).then("열살") .when(20).then("스무살") .otherwise("기타")) .from(member) .fetch();
Java
복사
복잡한 조건은 CaseBuilder 사용
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();
Java
복사
가급적 위와 같은 조건은 DB단에서 실행하지 않는 것이 바람직
orderBy에 사용할 경우
NumberExpression<Integer> rankPath = new CaseBuilder() .when(member.age.between(0, 20)).then(2) .when(member.age.between(21, 30)).then(1) .otherwise(3); List<Tuple> result = queryFactory .select(member.username, member.age, rankPath) .from(member) .orderBy(rankPath.desc()) .fetch();
Java
복사

상수

상수가 필요할 경우 Expression.constanct() 사용
List<Tuple> result = queryFactory .select(member.username, Expressions.constant("A")) .from(member) .fetch();
Java
복사

문자 더하기(concat)

List<String> result = queryFactory .select(member.username.concat("_").concat(member.age.stringValue())) .from(member) .where(member.username.eq("member1")) .fetch();
Java
복사
문자가 아닌 다른 타입들은 stringValue() 로 문자로 변환할 수 있음
이 방법은 ENUM을 처리할 때도 자주 사용

프로젝션(projection)

프로젝션 대상이 하나면 타입을 명확하게 지정 가능
List<String> result = queryFactory .select(member.username) .from(member) .fetch();
Java
복사
프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회
List<Tuple> result = queryFactory .select(member.username, member.age) .from(member) .fetch();
Java
복사
DTO 프로젝션
프로퍼티 접근 - Projections.bean()
List<MemberDto> result = queryFactory .select(Projections.bean( MemberDto.class, member.username, member.age)) .from(member) .fetch();
Java
복사
필드 직접 접근 - Projections.fields()
List<MemberDto> result = queryFactory .select(Projections.fields( MemberDto.class, member.username, member.age)) .from(member) .fetch();
Java
복사
DTO의 필드가 private이어도 사용 가능
별칭이 다를 때는 ExpressionUtils.as() 사용
List<UserDto> result = queryFactory .select(Projections.fields(UserDto.class, member.username.as("username"), ExpressionUtils.as( JPAExpressions .select(memberSub.age.max()) .from(memberSub), "age") )) .from(member) .fetch();
Java
복사
생성자 사용
List<UserDto> result = queryFactory .select(Projections.fields(UserDto.class, member.username.as("username"), ExpressionUtils.as( JPAExpressions .select(memberSub.age.max()) .from(memberSub), "age") )) .from(member) .fetch();
Java
복사
DTO 생성자 메서드에 @QueryProjection 사용 → 가장 편하고 많이 사용
@Data @NoArgsConstructor public class MemberDto { private String username; private int age; @QueryProjection public MemberDto(String username, int age) { this.username = username; this.age = age; } }
Java
복사
List<MemberDto> result = queryFactory .select(new QMemberDto(member.username, member.age)) .from(member) .fetch();
Java
복사
컴파일러로 타입 체크가 가능해 가장 안전
다만 DTO에 QueryDSL 의존성이 누출되는 단점 존재

Distinct

List<String> result = queryFactory .select(member.username).distinct() .from(member) .fetch();
Java
복사
JPQL의 distint와 역할 동일

동적 쿼리

Boolean builder 사용
private List<Member> searchMember1(String usernameCond, Integer ageCond) { BooleanBuilder builder = new BooleanBuilder(); if (usernameCond != null) { builder.and(member.username.eq(usernameCond)); } if (ageCond != null) { builder.and(member.age.eq(ageCond)); } return queryFactory .selectFrom(member) .where(builder) .fetch(); }
Java
복사
where문 다중 파라미터 사용
private List<Member> searchMember2(String usernameCond, Integer ageCond) { return queryFactory .selectFrom(member) .where( usernameCond != null ? member.username.eq(usernameCond) : null, ageCond != null ? member.age.eq(ageCond) : null ) .fetch(); }
Java
복사
아래처럼 메서드로 뽑으면 가독성 높이고 재활용 가능
@Test public void 동적쿼리_WhereParam() throws Exception { String usernameParam = "member1"; Integer ageParam = 10; List<Member> result = searchMember2(usernameParam, ageParam); Assertions.assertThat(result.size()).isEqualTo(1); } private List<Member> searchMember2(String usernameCond, Integer ageCond) { return queryFactory .selectFrom(member) .where(usernameEq(usernameCond), ageEq(ageCond)) .fetch(); } private BooleanExpression usernameEq(String usernameCond) { return usernameCond != null ? member.username.eq(usernameCond) : null; } private BooleanExpression ageEq(Integer ageCond) { return ageCond != null ? member.age.eq(ageCond) : null; }
Java
복사

수정, 삭제 벌크 연산

업데이트
long count = queryFactory .update(member) .set(member.username, "비회원") .where(member.age.lt(28)) .execute(); em.flush(); em.clear();
Java
복사
DB와 데이터가 달라지게 되는 영속성 컨텍스트를 초기화해줘야 함
간단한 연산
더하기, 곱하기 등 - 메서드 체인으로 add, multiply 등 연산 사용 가능
long count = queryFactory .update(member) .set(member.age, member.age.add(1)) .execute();
Java
복사
삭제
long count = queryFactory .delete(member) .where(member.age.lt(18)) .execute();
Java
복사

SQL Function 호출

Expressions.stringTemplate() 사용
List<String> result = queryFactory .select(Expressions.stringTemplate( "function('replace', {0}, {1}, {2})", member.username, "member", "M")) .from(member) .fetch();
Java
복사
lower 같은 ansi 표준 함수들은 QueryDSL이 상당부분 내장하고 있으니 그냥 아래처럼 사용하면 됨
.where(member.username.eq(member.username.lower()))
Java
복사