

Querydsl
Series
QueryDSL 이란?
Querydsl을 통해 생성되는 정적 Q-type 클래스를 이용해 쿼리를 생성하도록 도와주는 프레임 워크
JPQL vs Querydsl
타입 체크, 오류 잡아주는 시점
JPQL - 실행 시점 오류
Querydsl - 컴파일 시점 오류
파라미터 바인딩
JPQL - 파라미터 바인딩 직접
Querydsl - 파라미터 바인딩 자동 처리
build.gradle
build.gradle에 queryDSL 설정 추가
# build.gradle
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugin {
...
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
...
}
dependencies {
...
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
...
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
# queryDSL Q타입 생성 (git ignore 설정 필요)
# build.gradle에 설정한 명령어
./gradlew clean compileQuerydsl
# Gradle 의존관계 확인
./gradlew dependencies --configuration compileClasspath
Querydsl 라이브러리
- querydsl-apt: Querydsl 관련 코드 생성 기능 제공
- querydsl-jpa: querydsl 라이브러리
QueryDSL 활용
QueryDSL Bean 등록
repository 작성 시 JPA에 접근하기 위한 EntityManager와 QueryDSL을 사용하기 위한 JPAQueryFactory를 주입받아야 하는데
EntityManager의 경우 Spring에 Bean으로 등록되어 있어 생성자 주입으로 주입이 가능
queryFactory도 생성자 주입으로 주입 받고 싶을 경우엔 SpringBoot에 Bean으로 등록시키고 생성자 생성시 주입받으면 된다.
(AppConfig를 만들어 적용)
@Configuration
@RequiredArgsConstructor
public class Appconfig {
private final EntityManager em;
@Bean
public JPAQueryFactory queryFactory() {
return new JPAQueryFactory(em);
}
}
- JPA 접근 시 EntityManager 필요
- JPAQueryFatory는 QueryDSl 쿼리 작성 클래스
동시성 문제
스프링은 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도
트랜잭션 마다 별도의 영속성 컨텍스트 제공 -> 동시성 문제 x
Qclass 인스턴스 사용 방법
- 별칭 직접 지정 -> 같은 테이블 조인 시
- 기본 인스턴스 사용 -> static import로 전역선언 후 사용
//별칭 직접 지정
QMember qMember = new QMember("m");
//기본 인스턴스 사용
QMember qMember = QMember.member;
// 전역 선언
import static study.querydsl.entity.QMember.*;
검색 조건 추가
- .and(), .or() 메서드 체인으로 연결
- JPQL이 제공하는 모든 검색 조건 제공
- and vs eq -> eq 사용 - 동적 쿼리 만들 때 null 무시하게 코드 작성 가능
검색 조건 설정 (Toggle)
| 표현 | 결과 |
|---|---|
| .eq(“member”) | username = ‘member’ |
| .ne(“member”) | username != ‘member’ |
| .eq(“member”).not() | username != ‘member’ |
| .isNotNull() | is not null |
| .in(10, 20) | in (10,20) |
| .notIn(10, 20) | not in (10, 20) |
| .between(10,30) | between 10, 30 |
| .goe(30) | >= 30 |
| .gt(30) | > 30 |
| .loe(30) | <= 30 |
| .lt(30) | < 30 |
| .like(“member%“) | like 검색 |
| .contains(“member”) | like ‘%member%’ 검색 |
| .startsWith(“member”) | like ‘member%’ 검색 |
결과 조회
fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
fetchOne() : 단 건 조회
- 결과가 없으면 : null
- 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
fetchFirst() : limit(1).fetchOne()
fetchResults, fetchCount -> deprecated 되서 count용 쿼리 구현 후 사용
정렬
groupby 사용
desc() , asc() : 일반 정렬 nullsLast() , nullsFirst() : null 데이터 순서 부여
SQL은 다중 정렬 시 왼쪽부터 순차 정렬
.orderBy(member.age.desc(), member.username.asc().nullsLast())
집합
7가지 집합 함수 사용 가능
조인
기본 조인 (페치 조인 적용)
- join은 leftJoin, rightJoin 모두 가능
- join 이후에 on을 넣어 조건절 가능
- 세타조인 또한 가능
- .on() 절을 통해 조인 대상을 필터링 하거나 동적인 조건 연관관계 없이 조인 가능
List<Member> resultQuerydsl = queryFactory
.selectFrom(user)
.join(member.team, team).fetchJoin() // fetch join 가능
.fetch();
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
서브 쿼리
- QueryDSL에서 제공하는 JPAExpressions 클래스를 사용해 서브쿼리 사용 가능
- alias가 중복되면 안되는 경우 Q-type 객체 생성해서 사용
- case문 사용 시 CaseBuilder 클래스 사용
- 상수 사용 시 Expressions 클래스 사용
// 1. where절 서브쿼리
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
)).fetch();
// 2. select절 서브쿼리
List<Tuple> fetch = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
).from(member)
.fetch();
// 3. case문
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();
// 4. 상수 사용
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
프로젝션
- 간단한 몇 가지 프로퍼티들만 조회 할 경우엔 Q-type 객체의 프로퍼티를 넘겨주면 된다.
- Dto를 사용해 select 할 경우엔 3가지 방법이 가능하다
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
- 프로퍼티, 필드 접근 시 이름이 다를 땐 **ExpressionUtils.as(source,alias)**를 사용해 필드나 서브쿼리에 별칭을 적용한다.
// 대상이 하나인 경우
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
// 대상이 여러개
List<Tuple> result2 = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
// --- DTO 사용
// 1. Property 사용
// bean으로 사용하면 getter, setter로 값을 넣어준다.
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// 2. field 직접 접근
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
// 2.1 field 접근 시 별칭이 다를 때
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
// 서브 쿼리 사용 시, 이름이 없을 때 ExpressionUtils 사용
ExpressionUtils.as(
select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
// 3. 생성자 사용
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
추가로 Dto 클래스에 @QueryProjection 어노테이션을 추가해 Dto 전용 Q-type을 추가해
깔끔한 코드를 작성 가능한데 Dto 클래스 내부에 Querydsl 라이브러리 의존성이 생기기 때문에
확장성이나 유지보수 시 단점이 될 수 있다.
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
@QueryProjection
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
List<UserDto> result = queryFactory
.select(new QUserDto(member.username, member.age))
.from(member)
.fetch();
Tip!!
- Query로 데이터 가져올 때 기본조건이나 limit 조건 있는게 좋음 (너무 많은 데이터 가져와서)
동적 쿼리
동적 쿼리를 처리할 땐 BooleanBuilder, Where 다중 파라미터 2가지 방법으로 처리가 가능하다.
- BooleanBuilder 사용
List<Member> result = searchMember1(usernameParam, ageParam);
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();
}
- Where 다중 파라미터 사용
List<Member> result = searchMember2(usernameParam, ageParam);
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;
}
SQL 함수 사용
- concat, coalesce, upper 와 같은 간단한 함수들은 Querydsl 에서 메소드로 지원
- 자신이 사용하고 싶은 함수가 Querydsl 에 없는 경우는 stringTemplate()을 이용하여 쿼리를 작성 가능
ist<String> result = queryFactory
.select(Expressions.stringTemplate(
"function('regexp_replace', {0}, {1}, {2})",
user.username, "user", "User_"))
.from(user)
.fetch();
수정, 삭제 벌크 연산
- 영속성 컨텍스트에 있는 엔티티를 무시하고 실행되기 때문에 배치 쿼리를 실행하고 나면 영속성 컨텍스트를 초기화 필요
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age,lt(28))
.execute();
em.flush();
em.clear();
DataJPA와 함께 사용
DataJPA 같은 경우 실전 스프링 데이터 JPA 에 정리해놔서
코드로만 정리한다.
// repository
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
// Custom
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
Pageable pageable);
Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
Pageable pageable);
}
// Impl
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
//회원명, 팀명, 나이(ageGoe, ageLoe)
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.fetch();
}
private BooleanExpression usernameEq(String username) {
return isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe == null ? null : member.age.goe(ageGoe);
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe == null ? null : member.age.loe(ageLoe);
}
@Override
public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition,
Pageable pageable) {
QueryResults<MemberTeamDto> results = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetchResults();
List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total);
}
/*
* 데이터 조회 쿼리와, 전체 카운트 쿼리를 분리
*/
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition,
Pageable pageable) {
List<MemberTeamDto> content = queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name))
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// long total = queryFactory
// .select(member)
// .from(member)
// .leftJoin(member.team, team)
// .where(usernameEq(condition.getUsername()),
// teamNameEq(condition.getTeamName()),
// ageGoe(condition.getAgeGoe()),
// ageLoe(condition.getAgeLoe()))
// .fetchCount();
// return new PageImpl<>(content, pageable, total);
JPAQuery<Member> countQuery = queryFactory
.select(member)
.from(member)
.leftJoin(member.team, team)
.where(usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe()));
return PageableExecutionUtils.getPage(content, pageable,
countQuery::fetchCount);
}
}