[Spring] Querydsl 환경설정, 문법
환경설정
스프링 부트 3.0 이상 버전에서 적용해봤는데 다음 내용을 고려해주니 문제없이 동작했다!
- Java 17 사용
- IDE에서도 Java 버전과 관련된 설정이 잘 돼있는지 꼭 확인하기
- H2 데이터베이스 2.1.214 버전 사용
build.gradle
에도 querydsl 코드를 추가해줘야 한다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.0'
id 'io.spring.dependency-management' version '1.1.0'
//querydsl 추가
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
useJUnitPlatform()
}
//querydsl 추가
def querydslDir = "src/main/generated"
querydsl { jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
querydslDir
에 있는 내용은 git으로 관리되지 않도록 설정해줬음 (.gitignore
에서 설정)
querydsl에서 내가 만든 엔티티를 활용하려면 우측에 있는 [Gradle] - [compileQuerydsl]을 눌러서 컴파일 해주자.
그러면 위에서 설정한 querydslDir
에 파일이 생성될 것이며 (Member
엔티티라면 QMember
가 생성됨) querydsl
설정이 끝난다!
문법
jpql과 거의 유사하나 직접 쿼리를 작성하지 않고 함수 호출을 통해 작성하게 된다. 대략적인 폼은 같다!
@PersistentContext
EntityManager em;
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1")) //파라미터 바인딩
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
em
을JPAQueryFactory
생성자 파라미터로 넣어 객체 생성- querydsl에선 모든 쿼리를
JPAQueryFactory
에서 시작함 - 한 번만 생성
- querydsl에선 모든 쿼리를
QMember
를 생성해 쿼리 파라미터로 활용m
객체를new QMember("m")
대신QMember.member
로 대체 가능QMember.member
(=기본 인스턴스)는 static import를 적용해member
로 줄여서 사용 가능- 여러
Member
객체가 필요하다면new QMember()
로 생성해서 사용해야 함
- 쿼리에서 select/from/where절을 함수 형태로 호출 == 자바 코드 사용!
- querydsl == jpql 빌더
검색 조건
AND, OR 등의 조건은 and()
, or()
를 메서드 체인으로 연결할 수 있다.
= 연산은 eq()
로 처리한다.
queryFactory
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
select()
와from()
은selectFrom()
으로 합칠 수 있고 같은 내용이 들어갈 때만 사용 가능where()
에서null
은 무시
결과 조회
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
Member fetchOne = queryFactory
.selectFrom(member)
.fetchOne();
Member fetchFirst = queryFactory
.selectFrom(member)
.fetchFirst();
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
long count = queryFactory
.selectFrom(member)
.fetchCount();
fetch()
: 리스트 조회fetchOne()
: 단 건 조회- 없으면
null
리턴 - 여러개 있으면
NonUniqueResultException
발생
- 없으면
fetchFirst()
: 조건 만족하는 것 중 하나만 조회fetchResults()
: 페이징 정보 포함(= count 쿼리 추가 실행)fetchCount()
: count 수 조회
정렬
where()
뒤에 orderBy()
를 호출해서 설정할 수 있다.
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
desc()
는 내림차순,asc()
는 오름차순(기본값)nullsLast()
:null
이면 마지막으로 보냄
페이징
queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetch();
offset()
: 시작할 인덱스 설정(0부터 시작)limit()
: 조회할 대상 개수 설정
fetch()
대신 fetchResults()
를 썼다면 페이징 정보를 얻을 수 있지만 count 쿼리도 같이 나가게 되므로 성능에
영향을 미칠 수 있다. 성능 최적화가 필요하다면 count 전용 쿼리를 따로 만들어 사용해야 한다.
집합
합, 평균, 최대, 최소는 sum()
, avg()
, max()
, min()
으로 처리할 수 있다.
queryFactory
.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetch();
group by도 GroupBy()
로 처리가 가능하다! 뒤에 having()
도 사용할 수 있다.
queryFactory
.select(
team.name,
member.age.avg()
)
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
join()
으로member
와team
을 inner join 할 수 있음team
은QTeam.team
(=기본 인스턴스)을 의미
조인
inner join, left/right outer join 모두 사용 가능하다.
inner join은 join()
또는 innerJoin()
을,
outer join은 방향에 맞게 leftJoin()
, rightJoin()
을 사용하면 된다.
조건 설정은 뒤에 where()
이나 on()
에서 해주면 된다.
queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
세타 조인은 join()
을 사용하지 않고 from에 엔티티를 바로 적은 뒤 where()
에서 조건 설정해주면 된다.
queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
엔티티 간 연관관계가 없어도 조인을 할 수 있다. 연관된 필드가 없으므로 outer join을 해줘야 하며,
on()
을 활용해 조건을 설정할 수 있다.
queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
jpql에서는 페치 조인을 쿼리 안에 join fetch
를 써줬지만, querydsl에서는 fetchJoin()
을 사용한다.
queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
- join 함수(
join()
,leftJoin()
,rightJoin()
) 뒤에 사용
서브 쿼리
eq()
, goe()
, in()
등의 함수에서 파라미터로 서브 쿼리 내용을 작성해주면 된다.
단, 서브 쿼리는 반드시 JPAExpressions
로 시작해야한다.
//eq() 사용
queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
//goe() 사용
queryFactory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
//in() 사용
queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
select()
파라미터로도 들어갈 수 있고 방법은 같음- jpql이 from에서 서브쿼리를 지원하지 않아 querydsl도 지원하지 않음
- 서브쿼리를 join으로 변경
- 쿼리를 2개로 분리
- 네이티브 쿼리 사용
Case문
when()
, then()
으로 처리할 수 있으며 마지막 케이스는 otherwise()
로 처리한다.
복잡한 case문은 CaseBuilder
를 사용해주자.
//단순 쿼리
queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.when(30).then("서른살")
.otherwise("기타")
)
.from(member)
.fetch();
//복잡한 쿼리
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();
상수나 문자 더하기
상수를 더할 땐 Expressions.constant()
을, 문자를 더할 땐 concat()
을 사용한다.
queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
member.age
는 타입이String
이 아니기 때문에concat()
파라미터로 쓸 수 없음stringValue()
로 타입을String
으로 변경
프로젝션
원하는 결과만 가져올 수 있도록 설정하는걸 프로젝션이라 한다.
대상이 1개면 그 대상의 타입
을, 2개 이상이면 Tuple
을 리턴한다.
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
List<Tuple> result = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
Tuple
은get()
으로 참조 가능get(member.username)
,get(member.age)
등으로 활용 가능
DTO 조회
MemberDto
를 정의하고 querydsl에서 쿼리 결과가 Member
가 아닌 바로 MemberDto
로 얻는 방법을 알아보자.
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
public void setUsername(String username) {
this.username = username;
}
public void setAge(int age) {
this.age = age;
}
}
querydsl에서는 3가지 방법으로 DTO를 생성할 수 있다.
//Setter
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
//필드 직접 접근
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
//생성자
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
- 차례대로 Setter 사용, 필드에 직접 접근, 생성자 활용을 통해 DTO를 얻고 있음
또 @QueryProjection
를 사용해서 DTO를 편리하게 생성하는 방법이 있다.
MemberDto
생성자에 해당 어노테이션을 붙여줘야 한다.
//MemberDto
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
//쿼리
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
new QMemberDto()
로 바로 DTO 생성 가능- 하지만 DTO가
@QueryProjection
를 사용함으로써 querydsl에 의존한다는 점이 있음
동적 쿼리
동적 쿼리는 BooleanBuilder 또는 Where 다중 파라미터를 통해 해결할 수 있다.
private List<Member> useBooleanBuilder(String usernameParam, Integer ageParam) {
BooleanBuilder builder = new BooleanBuilder();
if(usernameParam != null) {
builder.and(member.username.eq(usernameParam));
}
if(ageParam != null) {
builder.and(member.age.eq(ageParam));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
private List<Member> whereMultipleParam(String usernameParam, Integer ageParam) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameParam), ageEq(ageParam))
.fetch();
}
private Predicate usernameEq(String usernameParam) {
if(usernameParam == null) {
return null;
}
return member.username.eq(usernameParam);
}
private Predicate ageEq(Integer ageParam) {
if(ageParam == null) {
return null;
}
return member.age.eq(ageParam);
}
BooleanBuilder
: 기준을 만들어서where()
파라미터에 삽입하는 방식Where 다중 파라미터
:where()
는null
을 무시한다는 점을 이용한 방식- 파라미터가
null
인 케이스에 한해서null
을 반환하고 있음 usernameEq()
,ageEq()
참조
- 파라미터가
벌크 연산
업데이트 쿼리를 작성해주되 벌크 연산처럼 영속성 컨텍스트를 무시하고 바로 DB에 업데이트하는 경우 일관성이 깨질
수 있기 때문에 강제로 초기화를 해줘야 한다.
//예1 : 나이가 20 미만이면 이름을 비회원으로 변경
queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(20))
.execute();
em.flush();
em.clear();
//예2 : 모든 회원의 나이를 1만큼 올리기
queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
em.flush();
em.clear();
execute()
를 마지막에 호출- 영속성 컨텍스트 초기화 :
em.flush()
,em.clear()
- 예2처럼 값 더할 땐
add()
사용하고, 곱할 땐multiply()
사용
SQL function
SQL에서 쓰던 함수를 querydsl에서도 사용할 수 있다.
다음은 replace 함수와 lower 함수를 사용할 때 예시이다.
/**
* 이름에서 member를 M으로 변경
*/
List<String> result = queryFactory
.select(Expressions.stringTemplate(
"function('replace', {0}, {1}, {2})",
member.username, "member", "M"))
.from(member)
.fetch();
/**
* 이름을 소문자로 변경
*/
List<String> result = queryFactory
.select(member.username)
.from(member)
// .where(member.username.eq(
// Expressions.stringTemplate("function('lower', {0})", member.username)))
.where(member.username.eq(member.username.lower()))
.fetch();
Expressions.stringTemplate()
을 호출해 SQL function 사용- lower 함수처럼 ANSI 표준 함수는 querydsl에 내장돼있어
lower()
사용 가능
댓글남기기