스프링 부트 2.6부터 Querydsl 5.0을 사용한다.
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
...
//querydsl 추가
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
...
dependencies {
//querydsl 추가
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
}
compileQuerydsl
을 클릭하면 @Entity
Annotation이 붙은 Class를 확인하면서 Qxxx
라는 이름의 클래스를 생성한다.
즉, Member
Entity가 존재하면 QMember
라는 클래스가 생기는 것이다.
QueryDSL은 Entity를 한 번 감싼(?) Q클래스
를 통해 데이터에 접근하는 것이다.
querydsl-apt
: QClass를 만드는 역할
querydsl-jpa
: 쿼리 작성 역할
유저 이름으로 조회하기
@Test
void jqplTest() {
String query = "SELECT m FROM Member m WHERE m.username = :username";
Member findMember = em.createQuery(query, Member.class)
.setParameter("username", "username#1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("username#1");
}
@Test
void queryDslTest() {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("username#1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("username#1");
}
@Test
void queryDslTest() {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("username#1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("username#1");
}
Spring이 주입하는 EntityManager
는 Thread Safe하기 때문에, JPAQueryFactory
를 필드로 추출해서 사용해도 된다.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
@Test
void queryDslTest() {
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("username#1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("username#1");
}
QueryDSL은 Q클래스
인스턴스를 통해 데이터에 접근한다.
Q클래스
인스턴스는 두 가지 방법으로 사용할 수 있다.
// 1. 별칭 직접 입력
// 입력한 별칭이 SQL문 Table의 별칭으로 사용된다.
// SELECT m1.username FROM Member AS m1
QMember qMember = new QMember("m1");
// 2. 기본 인스턴스 사용
// `Q클래스` 내부적으로 미리 초기화 해놓은 인스턴스를 가지고 있다.
QMember qMember = QMember.member;
2. 기본 인스턴스 사용
사용을 권장한다.
// 1. 기본 사용
QMember qMember = QMember.member;
// 2. Static Import로 간소화 하기
import static 경로.entity.QMember.*;
QMember member = member; // QMember.member를 간소화
같은 Table을 다룰 때는 Table을 구분해야 하므로, 그때 Alias 방식을 사용한다.
qf // queryFactory
.select(member)
.from(member)
.where(
member.username.eq("a") // username = "a"
member.username.ne("a") // username != "a"
member.username.eq("a").not // username != "a"
member.username.isNotNull() // username 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, 20) age BETWEEN 10 AND 20;
member.age.goe(30) // age >= 30 (greater or equal)
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30 (less or equal)
member.age.lt(30) // age < 30
member.username.like("member%") // like member%
member.username.contains("member") // like %member%
member.username.startsWith("member") // like member%
).fetchOne();
두 방식으로 And
조건을 설정할 수 있다.
// 1. and() 사용
qf
.select(member)
.from(member)
.where(member.username.eq("a").and(member.age.eq(10)))
// 2. 파라미터로 사용
qf.select(member)
.from(member)
.where(
member.username.eq("a"),
member.age.eq(10)
)
2. 파라미터로 사용
을 권장함
null
이 들어가면 알아서 무시함// 1. List로 조회
// selectFrom : Entity가 같은 경우 축약 가능 (select + from)
List<Member> members = qf.selectFrom(member).fetch();
// 2. 단건 조회
Member member = qf.selectFrom(member).fetchOne();
// 3. 첫번째 결과만 조회
Member member = qf.selectFrom(member).fetchFirst();
// 4. 복합 결과 (페이징)
// 페이징을 위해 count 개수를 검색하는 쿼리가 발생함 (총 쿼리 2번)
// 최신 버전에서는 deprecated
QueryResults<Member> results = qf.selectFrom(member).fetchResults();
// 5. 데이터 개수 조회 (count)
long total = qf.selectFrom(member).fetchCount();
나이 기준 오름차순, 이름 기준 내림차순, 이름이 null인 경우 뒤에 위치하도록 정렬
List<Member> fetch = qf
.selectFrom(member)
.orderBy(member.age.asc(), member.username.desc().nullsLast())
.fetch();
nullsLast
: 값이 null인 경우 마지막에 위치
nullsFirst
: 값이 null인 경우 앞에 위치
// 1페이지에서부터 3개 불러오기
List<Member> results = qf
.selectFrom(member)
.offset(1).limit(3).fetch();
QueryDSL도 0
부터 페이지가 시작한다.
List<Tuple> result = qf.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetch();
Tuple tuple = result.get(0);
Long count = tuple.get(member.count());
Integer sum = tuple.get(member.age.sum());
Double avg = tuple.get(member.age.avg());
Integer max = tuple.get(member.age.max());
Integer min = tuple.get(member.age.min());
Tuple
은 QueryDSL이 제공하는 자료구조이다.
팀 인원이 10명 이상인 팀의 이름과, 평균 나이
List<Tuple> result = qf
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.having(team.count().goe(10))
.fetch();
Tuple team1 = result.get(0);
Tuple team2 = result.get(2);
List<Member> result = qf
.select(member)
.from(member)
// join, innerJoin, leftJoin, rightJoin
// join을 수행하면 JPA를 따라 Inner Join 발생
.join(member.team, team) // QTeam.team
.fetch();
첫 번째 파라미터에 조인 대상
을 지정하고, 두 번째 파라미터 alias로 사용할 Q타입을 지정하면 된다.
JPA 2.1부터 On
을 사용할 수 있다.
회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인, 회원은 모두 조회
(JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name = 'A')
List<Tuple> result = qf
.selectFrom(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("A"))
.fetch();
Outer Join
이므로, 팀 A에 속하지 않는 멤버 결과가 포함된다. null
에 주의해야 한다.
Inner Join
을 사용할 때 필터링 조건을 On
에 걸든, Where
절에 걸든 결과는 동일하다.
따라서 일관된 스타일을 위해서, Inner Join
에서는 익숙한 Where
절을 사용하고, Outer Join
에 경우에만 On
절을 사용하도록 하자.
join
을 사용할 때 두 문법이 있다.
// 1. 일반 조인
qf
.select(member)
.from(member)
.join(member.team, team)
.fetch();
// 2. 조인대상만 사용 + On 절
qf
.select(member)
.from(member)
.join(team).on(member.team.id.eq(team.id))
.fetch()
첫 번째 방식처럼 사용하면 알아서 PK, FK 기준으로 ON절을 구성한다.
즉, SQL ON절에 member.team_id = team.id
가 추가된다.
두 번째 방식처럼 사용하면 알아서 PK, FK 기준으로 ON절을 구성하지 않는다.
그러므로 명시적으로 ON절을 설정해 줘야 한다.
Cross Join (Cartesian Product)
: N * M
List<Tuple> tuples = qf
.select(memeber, team)
.from(member, team)
.fetch();
// JPQL
SELECT member1, team FROM Member member1, Team team
Theta Join
: Caretsian Product에서 특정 조건 (세타 조건)을 만족하는 경우만 반환
유저 이름과 팀 이름이 같은 경우
List<Tuple> result = qf
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
// JQPL
SELECT member1, team FROM Member member1, Team team WHERE member1.username = team.name
// 그냥 연습용으로 비교
List<Member> members = qf
.select(member)
.from(member)
.where(member.username.eq(member.team.name))
.fetch();
// JPQL
// 묵시적 JOIN 발생
SELECT member1 FROM Member member1 WHERE member1.username = member1.team.name
// QueryDSL
List<Member> result = qf
.select(member)
.from(member)
.join(member.team, team).fetchJoin()
.where(team.name.eq("team1"))
.fetch();
// JPQL
SELECT m FROM Member m JOIN FETCH m.team t WHERE t.name = "team1"
어떤 Join이든지 fetchJoin()
을 같이 사용하면 된다.
fetchJoin을 사용하지 않아도 같은 효과를 낼 수 있다.
// QueryDSL
List<Tuple> team1 = qf
.select(member, member.team)
.from(member)
.join(member.team, team)
.where(team.name.eq("team1"))
.fetch();
다만 반환 타입이 Tuple
이다.
com.querydsl.jpa.JPAExpressions
를 사용하여 서브쿼리를 구성할 수 있다.
// 가장 나이가 많은 회원 조회
QMember subQMember = new QMember("sub_member");
List<Member> members = qf
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(subQMember.age.max())
.from(subQMember)
))
.fetch();
초반부에 다뤘던 것 처럼, 같은 Table(Entity)를 다루므로 Alias를 다르게 해서 구분해야 한다.
// SELECT절 Subquery
QMember subQMember = new QMember("sub_member");
List<Member> members = qf
.select(
member,username,
JPAExpressions
.select(subQMember.age.avg())
.from(subQMember))
.from(member)
.fetch();
역시나 JPAExpressions
도 static import로 분리하여 가독성을 높일 수 있다.
JPA, JPQL이 From절 Subquery를 지원하지 않으므로, QueryDSL도 불가능하다.
다음의 방법으로 해결 할 수 있을 것이다.
1. (가능한 경우) JOIN으로 변경하기
2. 쿼리를 두 번 날려서 변경하기
3. NativeSQL 사용하기
SELECT절 Subquery는 JPA는 불가능한데, Hibernate는 가능해서 QueryDSL도 가능한 것이다.
//Simple CASE Statement
List<String> ages = qf
.select(member.age
.when(10).then("ten")
.when(20).then("twenty")
.otherwise("??"))
.from(member)
.fetch()
// Searched CASE Statement
List<String> ages = qf
.select(new CaseBuilder()
.when(member.age.between(0,10)).then("ten")
.when(member.age.between(11,20)).then("twenty")
.otherwize("??"))
.from(member)
.fetch();
DB에서는 단순히 데이터만 조회하기를 권장한다.
// 상수 (Expressions)
List<Tuple> result = qf
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
// 문자 더하기 (concat)
List<String> result = qf
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.fetch();
상수는 JPQL, SQL에는 반영되지 않고, 최종 결과에서만 반영된다.
문자 더하기는 JPQL, SQL에도 반영되서 전달된다.