기본 Q-Type 활용
Q클래스 인스턴스를 사용하는 2가지 방법
QMember member = new QMember("m");// 직접 별칭 주어 새롭게 생성하여 사용
QMember member = QMember.member; // 이미 생성된 인스턴스 사용
새롭게 생성하여 사용하는 경우엔 생성자 안에 별칭을 넣어 사용하면 된다.
그러나, 기본적으로 생성된 인스턴스를 사용하는 것을 권장한다.
실제 사용 시 관련 Q Class 를 static import 하여 사용하는 것을 추천
import static study.querydsl.entity.QMember.*;
@DisplayName("QMember static import 하여 사용")
@Test
void querydsl() {
// given: QMember
String username = "member1";
// when
Member saved = queryFactory
.select(member)
.from(member)
.where(member.username.eq(username))
.fetchOne();
// then
assertThat(saved).isNotNull();
assertThat(saved.getUsername()).isEqualTo(username);
}
※ 참고사항
위에서 살펴본 두가지 방식 중 기본적으로 생성된 인스턴스를 사용하되,
만약 같은 테이블을 조인하여 사용해야하는 경우 서로 다른 별칭을 주어 인스턴스 생성하여 사용 해야한다.
검색 조건 쿼리
SQL 에서 제공되는 대부분의 조건문들은 Querydsl 을 통해서 사용할 수 있다.
특히, 기존 JPQL 에서 제공되는 것들은 모두 사용할 수 있다고 보면 된다.
(Querydsl 로 작성된 쿼리는 결과적으로 JPQL 로 변환되어 요청되기 때문에)
@DisplayName("검색 조건 쿼리")
@Test
void search() {
// given: QMember
int age = 10;
String username = "member1";
// when
Member saved = queryFactory
.selectFrom(member)
.where(member.username.eq(username)
.and(member.age.eq(age)))
.fetchOne();
// then
assertThat(saved).isNotNull();
assertThat(saved.getUsername()).isEqualTo(username);
assertThat(saved.getAge()).isEqualTo(age);
}
참고로, and 조건의 경우 파라미터 단위로 끊어서 사용 가능하다
// when
Member saved = queryFactory
.selectFrom(member)
.where(member.username.eq(username), member.age.eq(age))
.fetchOne();
※ 참고 - and 조건을 파라미터로 활용 시 null 인 경우 해당 조건 무시함 (동적 쿼리 작성 시 유용함)
Querydsl 제공되는 검색 조건 예시
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, 30
member.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%’ 검색
결과 조회
- fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne() : 단 건 조회
- 결과가 없으면 : null
- 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
- fetchFirst() : limit(1).fetchOne()
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount() : count 쿼리로 변경해서 count 수 조회
@DisplayName("결과 조회")
@Test
void resultFetch() {
// 리스트
List<Member> members = queryFactory.selectFrom(member)
.fetch();
// 단건
Member fetchOne = queryFactory.selectFrom(QMember.member)
.fetchOne();
// 처음 한건만 조회
Member fetchFirst = queryFactory.selectFrom(QMember.member)
.fetchFirst();
// 페이징에서 사용
QueryResults<Member> results = queryFactory.selectFrom(QMember.member)
.fetchResults();
long total = results.getTotal(); // total count 용 쿼리 추가 발생 (복잡한 경우 따로 count 쿼리 사용 추천)
List<Member> content = results.getResults();
// count 쿼리로 변경
long count = queryFactory.selectFrom(QMember.member)
.fetchCount();
}
정렬
@DisplayName("정렬 예시")
@Test
void sort() {
// given
em.persist(new Member(null, 100));
em.persist(new Member("member5", 100));
em.persist(new Member("member6", 100));
List<Member> results = queryFactory.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
assertThat(results).isNotNull();
Member member5 = results.get(0);
Member member6 = results.get(1);
Member memberNull = results.get(2);
assertThat(member5.getUsername()).isEqualTo("member5");
assertThat(member6.getUsername()).isEqualTo("member6");
assertThat(memberNull.getUsername()).isNull();
}
예시 코드 정렬 조건으로는 다음과 같다.
- 회원 나이 내림차순
- 회원 이름 오름차순
- 만약 이름이 없는 경우 맨 마지막에 출력(nulls last)
※ 참고 - nullsFirst 조건도 존재한다.
페이징
@DisplayName("기본 페이징")
@Test
void paging1() {
// given
int offset = 1;
int limit = 2;
// when
List<Member> result = queryFactory.selectFrom(member)
.orderBy(member.username.desc())
.offset(offset) // 0부터 시작
.limit(limit) // 요청 개수
.fetch();
// then
assertThat(result).isNotNull();
assertThat(result.size()).isEqualTo(limit);
}
@DisplayName("페이징 - 전체 개수가 필요한 경우")
@Test
void paging2() {
// given
int offset = 1;
int limit = 2;
// when
QueryResults<Member> result = queryFactory.selectFrom(member)
.orderBy(member.username.desc())
.offset(offset) // 0부터 시작
.limit(limit) // 요청 개수
.fetchResults();
// then
assertThat(result).isNotNull();
assertThat(result.getTotal()).isEqualTo(4);
assertThat(result.getLimit()).isEqualTo(limit);
assertThat(result.getOffset()).isEqualTo(offset);
assertThat(result.getResults().size()).isEqualTo(2);
}
페이징 쿼리 사용 시 유의 사항
fetchResult 의 경우 count 쿼리가 추가로 요청된다.
그렇지만, 복잡한 페이징인 경우 (where, join 등) 따로 더 간단하게 count 쿼리 작성할 수 있는 경우,
따로 count 쿼리 분리하여 사용하는게 성능 개선에 효과를 줄 수 있다.
집합
@DisplayName("집합")
@Test
void aggregation() {
// given: QMember
// when
Tuple result = queryFactory
.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetchOne();
// then
assertThat(result).isNotNull();
assertThat(result.get(member.count())).isEqualTo(4);
assertThat(result.get(member.age.sum())).isEqualTo(100);
assertThat(result.get(member.age.avg())).isEqualTo(25);
assertThat(result.get(member.age.max())).isEqualTo(40);
assertThat(result.get(member.age.min())).isEqualTo(10);
}
@DisplayName("GroupBy 사용 - 팀 이름과 각 팀의 평균 연령")
@Test
void groupBy() {
// given
// when
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
// then
Tuple teamA = result.get(0);
Tuple teamB = result.get(1);
assertThat(teamA.get(team.name)).isEqualTo("teamA");
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
assertThat(teamB.get(team.name)).isEqualTo("teamB");
assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
특이사항으로는 기본적으로 Tuple 객체를 반환한다
원하는 결과값은 해당 expression 을 get() 메서드에 넘겨주어 확인할 수 있다.
그렇지만 대게, Tuple 로 받아서 처리하기 보다 관련 DTO 로 받아 사용하는 것을 권장한다.(중급 사용에서 다룰 예정)
조인
기본 조인
@DisplayName("기본 조인 - Team A 에 소속된 모든 회원")
@Test
void join() {
// given: QMember, QTeam
// when
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
// then
assertThat(result).isNotNull();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
join 뿐만 아니라, leftJoin, rightJoin, innerJoin 등 모두 사용 가능하다.
참고 사항 - 연관관계가 없는 조인 가능 (세타 조인)
List<Member> result = queryFactory
.selectFrom(member)
.join(member, team)
.where(member.username.eq(team.name))
.fetch();
from 절에 여러 엔티티를 선택해서 연관관계가 없는 조인이 가능하다.
과거에는 외부 조인이 불가능하였으나(innerJoin 만 가능), 조인 on 을 사용하여 외부 조인 사용 가능
조인 - ON 절
ON 절을 활용한 조인은 참고로 JPA 2.1 부터 지원이 된다.
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인
조인 대상 필터링
@DisplayName("회원과 팀을 조인하면서, 팀 이름이 teamA 인 팀만 조인, 회원은 모두 조회")
@Test
void join_on_filtering() {
// given
String team = "teamA";
// when
List<Tuple> results = queryFactory
.select(member, QTeam.team)
.from(member)
.leftJoin(member.team, QTeam.team).on(QTeam.team.name.eq(team))
.fetch();
// then
assertThat(results).isNotNull();
results.forEach(result -> System.out.println("result = " + result));
}
ON 을 활용한 필터링은 외부 조인 인 경우에만 의미가 있다.
내부 조인을 하는 경우 WHERE 조건을 통해 해당 필터링을 동일하게 적용할 수 있다.
외부 조인이 필요한 경우에만 ON 절을 활용하고, 내부 조인인 경우 WHERE 을 활용하는 방식을 권장
// 내부 조인 ON 절 사용
List<Tuple> results = queryFactory
.select(member, team)
.from(member)
.join(member.team, team).on(team.name.eq(teamName))
.fetch();
// 결과
result = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
result = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
// 내부 조인 WHERE 절 사용
List<Tuple> results = queryFactory
.select(member, team)
.from(member)
.join(member.team, team)
.where(team.name.eq(teamName))
.fetch();
// 결과
result = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
result = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
연관관계 없는 엔티티 외부 조인
@DisplayName("연관관계가 없는 조인")
@Test
void join_on_no_relation() {
// given
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
em.persist(new Member("teamC"));
// when
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.fetch();
// then
assertThat(result).isNotNull();
result.forEach(tuple -> System.out.println("tuple = " + tuple));
}
외부 조인뿐만 아니라 내부 조인 역시 가능하다. (Hibernate5.1 부터 지원)
다만, 내부 조인인 경우 Team 이 null 인 경우 결과로 포함되지 않는다.
연관관계 없는 조인인 경우 문법이 조금 다른데, 연관관계가 있는 경우엔 leftJoin(member.team, team) 과 같이 작성하지만,
위처럼 연관관계가 없는 경우엔 from(member).leftJoin(team).on(조건) 조인 부분에 team 만 작성한다.
조인 - Fetch Join
Fetch Join 은 SQL 에서 제공하는 기능은 아니다.
JPQL 의 성능 튜닝을 위해 제공되는 조인 기능이며, 주로 성능 최적화에 사용하는 방식이다.
이는 SQL 조인을 활용해서 연관된 엔티티 혹은 컬렉션들을 한번에 조회하는 기능을 제공한다.
@DisplayName("페치조인 미적용")
@Test
void noFetchJoin() {
// given: QMember
em.flush();
em.clear();
String memberName = "member1";
// when
Member saved = queryFactory
.selectFrom(member)
.where(member.username.eq(memberName))
.fetchOne();
// then
assertThat(saved).isNotNull();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(saved.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();
}
@DisplayName("페치조인 적용")
@Test
void useFetchJoin() {
// given: QMember
em.flush();
em.clear();
String memberName = "member1";
// when
Member saved = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq(memberName))
.fetchOne();
// then
assertThat(saved).isNotNull();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(saved.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();
}
Fetch JOIN 을 적용하지 않은 경우 현재 Member 와 Team 의 연관관계 로딩 전략은 Lazy를 취하고 있기 때문에,
Member 조회 시 해당 Team 엔티티를 같이 조회하지 않는다. 실제 Team 엔티티에 접근 시 쿼리 요청을 통해 가져오는 방식을 취하고 있다.
즉, Member, Team 조회에 대한 SQL 쿼리가 각각 실행되는 것이다. (N+1 문제 야기)
반면, Fetch JOIN 을 적용한 경우 Member 에 필요한 Team 객체를 SQL 조인 쿼리로 한번에 조회하기 때문에,
조회 시점에서 바로 연관된 Team 엔티티에 접근할 수 있다.
사용 방법으로는 join(), leftJoin() 등의 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.
서브 쿼리
@DisplayName("WHERE 절 - 나이가 가장 많은 회원 조원")
@Test
void subQuery() {
// given
QMember memberSub = new QMember("memberSub");
// when
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
select(memberSub.age.max())
.from(memberSub)
))
.fetch();
// then
assertThat(result).extracting("age")
.containsExactly(40);
}
@DisplayName("WHERE 절 - 나이가 평균 이상인 회원")
@Test
void subQueryGoe() {
// given
QMember memberSub = new QMember("memberSub");
// when
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(memberSub.age.avg()).from(memberSub)
))
.fetch();
// then
assertThat(result).extracting("age")
.containsExactly(30, 40);
}
@DisplayName("IN 절 서브쿼리 사용하기")
@Test
void subQueryIn() {
// given
QMember memberSub = new QMember("memberSub");
// when
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
// then
assertThat(result).extracting("age")
.containsExactly(20, 30, 40);
}
@DisplayName("SELECT 절에 서브 쿼리 사용하는 경우")
@Test
void selectSubQuery() {
// given
QMember memberSub = new QMember("memberSub");
// when
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg())
.from(memberSub))
.from(member)
.fetch();
// then
assertThat(result).isNotNull();
result.forEach(tuple -> System.out.println("tuple = " + tuple));
}
위 예제 코드에선 static import 하여 보이진 않지만,
서브쿼리는 쓰고자 하는 곳에 com.querydsl.jpa.JPAExpressions 사용하여 작성한다. (SELECT, WHERE, IN)
JPAExpressions.select().where().from()
여기서 주의해야할 사항으로는 기존에는 생성된 QClass 사용하였지만,
다른 별칭을 준 새로운 인스턴스를 사용해야 한다. (각 예제 서브 쿼리 중 meberSub 참고)
이는 SQL 작성 시 다른 별칭을 사용하는거와 동일한 원리이다.
추가로, 현재 JPA 및 Querydsl 은 from 절의 서브 쿼리 작성을 지원하지 않는다.
JPA 는 SELECT 절 쿼리 지원하지 않지만, Querydsl 은 Hibernate 구현체를 사용하여 해당 기능을 제공한다.
FROM 절의 서브쿼리 해결방안으로는 다음 3가지가 존재한다.
- 서브 쿼리를 join 으로 변경한다. (가능한 상황인 경우)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.(성능상 이슈가 없을 시)
- 마지막으로 nativeSQL 을 사용한다.
대부분 from 절에 서브 쿼리 쓰는 이유 - 화면 단에 맞추기위한 데이터를 가져오기 위해
화면에 맞춘 쿼리는 올바른 요청이 아니다. SQL 오로지 데이터 가져오는데 집중하는게 좋다.
복잡한 쿼리를 줄이기 위해 데이터만 집중하는게 중요하다 (필요한 데이터만 출여서 가져오는 것)
CASE 문
@DisplayName("단순한 조건")
@Test
void basicCase() {
// given: QMember
// when
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
// then
assertThat(result).isNotNull();
result.forEach(age -> System.out.println("age = " + age));
}
@DisplayName("복잡한 조건")
@Test
void complexCase() {
// given: QMember
// when
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();
// then
assertThat(result).isNotNull();
result.forEach(age -> System.out.println("age = " + age));
}
JPQL 과 마찬가지로 CASE 문에 대해서 지원한다.
살펴볼 내용으로는 복잡한 CASE 문에 대해선 CaseBuilder 를 통해 정의하여 사용한다.
상수, 문자 더하기
@DisplayName("상수 집어넣기")
@Test
void constant() {
// given
// when
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
// then
assertThat(result).isNotNull();
result.forEach(tuple -> System.out.println("tuple = " + tuple));
}
@DisplayName("문자 더하기")
@Test
void concat() {
// given: QMember
// when 원하는 문자열 = {username}_{age}
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
// then
assertThat(result).isNotNull();
result.forEach(member -> System.out.println("member = " + member));
}
상수 집어넣기에 경우 실제 요청되는 JPQL 엔 해당 부분이 포함되지 않는다.
문자 더하기에선 .stringValue() 부분을 많이 쓰게 되는데, 문자가 아닌 다른 타입들을 문자열 변환할 때 사용한다.
특히 ENUM 처리 시에 자주 사용된다.
참고
- 해당 게시글은 김영한님의 실전! Querydsl 을 참고하여 작성되었습니다.
- 보다 자세한 내용은 해당 강의를 보시고 확인하시길 적극 권장 드립니다.
'JPA' 카테고리의 다른 글
Querydsl 실무 활용편 (1) (0) | 2021.05.02 |
---|---|
Querydsl 중급 문법 (0) | 2021.04.28 |
Querydsl 사용 개요 (0) | 2021.04.25 |
쿼리 메소드 기능 (0) | 2021.01.26 |
객체지향 쿼리 언어 (0) | 2021.01.25 |
댓글