예제 도메인 모델은 Spring Data JPA에서 사용한 모델링이랑 같다.
JPQL vs Querydsl
@Test
public void startJPQL() throws Exception {
Member findByJPQL = em.createQuery("select m from Member m where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
Assertions.assertThat(findByJPQL.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl() {
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = QMember.member;
Member findByQuerydsl = queryFactory.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
Assertions.assertThat(findByQuerydsl.getUsername()).isEqualTo("member1");
}
JPQL과 Querydsl 테스트하는 코드의 차이이다.
Querydsl 장점
- 컴파일시점에 오류를 잡아낸다. (JPQL은 메소드가 실행될 때 오류(Runtime Error)를 발견)
- 자바 코드로 쿼리를 작성할 수 있다.
- 파라미터 바인딩을 자동으로 해결해 준다.
- Code Assistant도 잘되어있다.
기본 Q-Type 활용
Q클래스 인스턴스를 사용하는 2가지 방법
QMember m = new QMember("m");
QMember m = QMember.member;
참고로 QMember.member은 QMember객체에 만들어놓은 객체이다.
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QMember extends EntityPathBase<Member> {
...
public static final QMember member = new QMember("member1");
}
코드를 더 심플하게 하는 방법
import static study.querydsl.entity.QMember.*;
위와 같이 import를 해주면 아래와 같이 코드를 만들 수 있다.
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
Member findByQuerydsl = queryFactory.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
Querydsl 쿼리문 보는법
application.yml에 아래와 같이 옵션을 추가해주어야한다.
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/querydsl
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
#show_sql: true
format_sql: true
use_sql_comments: true #querydsl 쿼리문 보는 옵션
검색 조건 쿼리
기본 검색 쿼리
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
기본 검색 조건은 .and(), or()를 메서드 체인으로 연결할 수 있다.
select, from을 selectfrom으로 합칠 수 있음.
JPQL이 제공하는 모든 검색 조건 제공
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%’ 검색
AND 조건을 파라미터로 처리
@Test
public void searchAndParam() {
List<Member> result1 = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"),
member.age.eq(10))
.fetch();
assertThat(result1.size()).isEqualTo(1);
}
where()에 파라미터로 검색 조건을 추가하면 AND조건이 추가된다.
결과 조회
- fetch()
- list 조회, 데이터 없으면 빈 리스트 반환
- fetchOne()
- 단건 조회
- 결과가 없으면 Null
- 결과가 둘 이상이면 : NonUniqueResultException
- fetchFirst()
- limit(1).fetchOne()
- fetchResults()
- 페이징 정보 포함, total count 쿼리 추가 발생
- fetchCount()
- count 쿼리로 변경해서 count 수 조회
//List
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
//단 건
Member findMember1 = queryFactory
.selectFrom(member)
.fetchOne();
//처음 한 건 조회
Member findMember2 = queryFactory
.selectFrom(member)
.fetchFirst();
//페이징에서 사용
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
//count 쿼리로 변경
long count = queryFactory
.selectFrom(member)
.fetchCount();
정렬
/**
* 회원 정렬 순서
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 올림차순(asc)
* 단, 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
*/
@Test
public void sort() throws Exception {
//given
em.persist(new Member(null, 100));
em.persist(new Member("member5", 100));
em.persist(new Member("member6", 100));
//when
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
//then
Member member5 = result.get(0);
Member member6 = result.get(1);
Member memberNull = result.get(2);
Assertions.assertThat(member5.getUsername()).isEqualTo("member5");
Assertions.assertThat(member6.getUsername()).isEqualTo("member6");
Assertions.assertThat(memberNull.getUsername()).isNull();
}
위와 같이 orderBy안에 파라미터로 정렬 할 수 있다.
페이징
조회 건수 제한
@Test
public void paging(){
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) // 0부터 시작
.limit(2) // 최대 2건 조회
.fetch();
assertThat(result.size()).isEqualTo(2);
}
전체 조회 수가 필요하다면??
fetch() 대신 fetchResults()로 바꿔주면 된다.
주의
count 쿼리가 실행되니 성능상 주의해야 한다.
성능이 나오지 않는다면, count 쿼리를 분리해야 한다.
집합
집합 함수
@Test
public void aggregation() {
List<Tuple> result = queryFactory
.select(
member.count(), // 개수
member.age.sum(), // 합
member.age.avg(), // 평균
member.age.max(), // 가장 큰 값
member.age.min()) // 가장 작은 값
.from(member)
.fetch();
//Tuple은 querydsl이 제공하는 값
Tuple tuple = result.get(0);
assertThat(tuple.get(member.count())).isEqualTo(4);
assertThat(tuple.get(member.age.sum())).isEqualTo(100);
assertThat(tuple.get(member.age.avg())).isEqualTo(25);
assertThat(tuple.get(member.age.max())).isEqualTo(40);
assertThat(tuple.get(member.age.min())).isEqualTo(10);
}
위와 같이 java 코드처럼 사용가능하다.
GroupBy
@Test
public void group() throws Exception {
//given
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name) // 팀 이름으로 그룹
.fetch();
//when
Tuple teamA = result.get(0);
Tuple teamB = result.get(1);
//then
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);
}
groupBy, 그룹화된 결과를 제한하려면 having
GroupBy, having
.groupBy(team.name) // 팀 이름으로 그룹
.having(team.name.eq("teamB"))
위처럼 "teamB"와 같은 team만 가져올 수 있습니다.
조인 - 기본 조인
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() throws Exception {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result).extracting("username").containsExactly("member1", "member2");
}
위의 방식으로 join을 사용할 수 있다. leftjoin, rightjoin 모두 사용 가능하다.
연관관계가 없는 경우 from 절에 모든 데이터를 가져온 뒤, where 문으로 theta join할 수 있다.
조인 - on절
on절을 활용한 조인
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인
조인 대상 필터링
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* jpql : select m, t from Member m left join m.team t on t.name = "teamA"
*/
@Test
public void join_on_filtering() throws Exception {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.join(member.team, team)
.on(team.name.eq("teamA"))
.fetch();
for(Tuple tuple : result){
System.out.println("tuple = " + tuple);
}
}
연관관계 없는 엔티티 외부 조인
/**
* 연관관계가 없는 엔티티 외부 조인
* 회원의 이름이 팀 이름과 같은 대상 외부 조인
* jpql : select m, t from Member m LEFT JOIN Team t on m.username = t.name
*/
@Test
public void join_on_no_relation() throws Exception {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
em.persist(new Member("teamC"));
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team)
.on(member.username.eq(team.name))
.fetch();
for(Tuple tuple : result){
System.out.println("tuple = " + tuple);
}
}
주의해야 할 점은 leftjoin() 부분에 일반 조인과 다르게 엔티티가 하나만 들어갔다.
- 일반 조인 : leftJoin(member.team, team)
- on 조인 : from(member).leftJoin(team).on(~~~)
위의 방식으로 조인 문법이 다르다.
조인 - 페치 조인
@Test
public void fetchjoinUse() throws Exception {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치조인 미적용").isTrue();
}
join().fetchJoin() 을사용하면 fetch join이 된다.
서브 쿼리
com.querydsl.jpa.JPAExpressions 사용
서브 쿼리 eq 사용
/**
* 나이가 가장 많은 회원 조회
*/
@Test
public void subQuery() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
assertThat(result.get(0).getAge()).isEqualTo(40);
}
같은 QType을 쓰면안되기때문에, 서브쿼리에 넣을 QType은 새롭게 만들어서 넣어줘야 한다.
서브 쿼리 goe 사용
/**
* 나이가 평균 이상인 회원
*/
@Test
public void subQueryGoe() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
assertThat(result.size()).isEqualTo(2);
}
goe는 >= 라고 보면된다.
서브 쿼리 In 사용
/**
* 나이가 10 초과인 회원
*/
@Test
public void subQueryIn() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
assertThat(result.size()).isEqualTo(3);
}
select 절에 subquery
/**
* 나이가 10 초과인 회원
*/
@Test
public void selectSubQuery() {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub))
.from(member)
.fetch();
for(Tuple tuple : result){
System.out.println("tuple = " + tuple);
}
}
from 절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
from 절의 서브쿼리 해결방안
- 서쿼리를 Join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다고 한다.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다.
from절의 서브 쿼리를 줄이기 위한 책을 추천해주었다. SQL AntiPattern 나중에 읽을 기회가 된다면 사서 봐보자.
Case 문
@Test
public void basicCase() {
List<String> result = queryFactory
.select(member.age.when(10).then("열살")
.when(20).then("스무살")
.otherwise("서른살 이상"))
.from(member)
.fetch();
for(String s : result){
System.out.println("s = " + s);
}
}
@Test
public void complexCase() {
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();
for(String s : result){
System.out.println("s = " + s);
}
}
간단한 case문 예제이다.
orderyBy에서 Case문 함께 사용하기
예를들어 아래와 같은 임의의 순서로 회원을 출력하고 싶다면?
- 0~30살이 아닌 회원을 가장 먼저 출력
- 0~20살 회원 출력
- 21~30살 회원 출력
@Test
public void addCase() {
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();
for (Tuple tuple : result) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
Integer rank = tuple.get(rankPath);
System.out.println("username = " + username + " age = " + age + " rank = "
+ rank);
}
}
Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select절, orderBy절에서 사용할 수 있다.
상수, 문자 더하기
상수 더하기
/**
* 상수 더하기
*/
@Test
public void constant() {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for(Tuple tuple : result){
System.out.println("tuple = " + tuple);
}
}
Expressions.constant()를 사용하면 된다.
문자 더하기
/**
* 문자 더하기
*/
@Test
public void concat() {
//{username}_{age}
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
for(String s : result){
System.out.println("s = " + s);
}
}
가져온 값을 .stringValue()를 사용하며 string 변환하면 더할 수 있다.
출처
'spring > 인프런 강의 정리' 카테고리의 다른 글
[Querydsl] 4. 실무 활용 - 순수 JPQ와 Querydsl (0) | 2022.05.19 |
---|---|
[Querydsl] 3. 중급 문법 (0) | 2022.05.18 |
[Querydsl] 1. 프로젝트 세팅 (0) | 2022.05.17 |
[Spring Data JPA] 4. 확장 기능 (0) | 2022.05.16 |
[Spring Data JPA] 3. 쿼리 메소드 기능 (0) | 2022.05.14 |