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
복사