현업에서 Spring Data JPA + Querydsl 은 거의 무조건 사용되므로 잘 이해하고 사용하자.
Querydsl 장점
- 쿼리를 자바 코드로 작성하여 문법 오류를 컴파일 시점에 잡음
- 동적 쿼리 문제 해결
- 쉬운 SQL 스타일 문법
build.gradle 설정
Spring Boot 2.6 이상 버전에서는 Querydsl 5.0 을 사용하므로
아래와 같이 querydsl-jpa
, querydsl-apt
을 추가하고, 버전을 명시해주어야 한다.
- querydsl-jpa : 실제 애플리케이션에서 Querydsl 을 사용할때 필요한 라이브러리
- querydsl-apt : Q 클래스를 만드는 용도로 쓰임
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'org.springframework.boot' version '2.6.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
//querydsl 추가
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
id 'java'
}
group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
//querydsl 추가
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
참고 ) Querydsl 로 생성되는 Q 클래스는 git 에 올리면 안된다.
보통 build 폴더는 gitIgnore 하므로$buildDir/generated/querydsl
위치에 넣으면 됨
위와 같이 설정하고 compileQuerydsl 을 클릭하면 Q 클래스가 생성된다.
Test 코드 작성 시 @Commit 과 @Rollback(false) 의 차이
기본적으로 테스트 코드는 테스트 완료 시, 트랜잭션 rollback 을 하기때문에 DB 에서 결과를 볼 수 없다.
테스트 코드에서 엔티티가 실제 테스트 DB 에 잘 반영이 되었는지 눈으로 보고싶다면@Commit
이나 @Rollback(false)
를 사용하면 된다.
해당 어노테이션을 넣어주게 되면 테스트 완료 시 롤백을 하지 않는다.
참고로 @Commit 내부를 까보면 @Rollback(false) 를 사용하고 있기 때문에 둘의 기능은 동일하다.
Q 클래스는 static import 해서 사용하자
Q 클래스는 new QMember() 로 생성 할 필요 없이 static 하게 QMember.member 로 생성할 수 있다.
QMember m = QMember.member;
Member findMember = queryFactory.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
하지만 변수로 뽑지않고 static import 를 이용해서 깔끔하게 처리 할 수 있다.
import static study.querydsl.domain.QMember.member;
...
Member findMember = queryFactory.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
Querydsl 로 실행되는 JPQL 을 보고싶다면?
application.yml 에 use_sql_comments
옵션을 넣어주면 된다.
Querydsl 로 작성한 코드는 JPQL 의 빌더 역할이기 때문에 결국 JPQL 로 변환된다.
실행하면 아래와 같이 JPQL 코드를 볼 수 있다.
alias (별칭) 가 member1 로 찍힌 이유
Querydsl 이 자동으로 만들어주는 Q 클래스의 alias 가 member1 로 지정되어 있기 때문
같은 테이블을 조인해야 하는 상황에서는 alias 가 달라야 하므로 아래와 같이 직접 선언하면 된다.
보통 이렇게 사용할 일은 많이 없긴하다.
QMember m1 = new QMember("m1");
select, selectFrom
Member findMember = queryFactory.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
위와 같이 select 와 from 절이 같은 경우는 아래와 같이 selectFrom
으로 줄여 쓸 수 있다.
Member findMember = queryFactory.selectFrom(member)
.where(member.username.eq("member1"))
.fetchOne();
where 절 and
and 방식
Member findMember = queryFactory.selectFrom(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
쉼표 (,) 방식
Member findMember = queryFactory.selectFrom(member)
.where(
member.username.eq("member1"),
member.age.eq(10)
)
.fetchOne();
and 와 쉼표 방식 모두 and 로 동작한다.
둘 중 어느걸 쓰는게 더 좋냐 하는 것은 취향 차이지만,
영한님은 쉼표 방식이 좀 더 깔끔하고, null 이 들어가도 무시하므로 동적 쿼리를 깔끔하게 짤 수 있어서 선호하신다고 함
결과 조회
- fetch() : 리스트 조회, 데이터가 없으면 빈 리스트를 반환
- fetchOne() : 단 건 조회
- 결과가 없으면 : null
- 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException 발생
- fetchFirst() : limit(1).fetchOne()
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행 (select 쿼리 2번 나감 - 컨텐츠 쿼리 1번, total count 쿼리 1번)
- 페이징 쿼리가 복잡해지면 컨텐츠 쿼리와 total count 쿼리가 다를 경우가 있다. (성능 최적화로 total count 쿼리를 심플하게 만듬)
이런 복잡하고 성능이 중요한 페이징 쿼리에서는 fetchResults 를 쓰면 안된다. (쿼리 2번을 날려야한다.) - Querydsl 5.0 부터 Deprecated
- 페이징 쿼리가 복잡해지면 컨텐츠 쿼리와 total count 쿼리가 다를 경우가 있다. (성능 최적화로 total count 쿼리를 심플하게 만듬)
- fetchCount() : count 쿼리로 변경해서 count 수 조회
- Querydsl 5.0 부터 Deprecated
fetchResults(), fetchCount() Deprecated
Querydsl 의 fetchResults()
, fetchCount()
는 개발자가 작성한 select 쿼리를 기반으로 count 용 쿼리를 내부에서 만들어서 실행한다.
하지만 이 기능은 select 구문을 단순히 count 처리하는 용도로 바꾸는 정도다.
따라서 단순한 쿼리에서는 잘 동작하지만, 복잡한 쿼리에서는 제대로 동작하지 않는다.
그래서 Querydsl 은 fetchResults(), fetchCount() 를 Deprecated 하기로 결정했다.
count 쿼리가 필요하다면 다음처럼 별도로 작성해야 한다.
Long count = queryFactory.select(member.count())
.from(member)
.fetchOne();
member.count()
를 사용하면 count(member.id) 로 처리된다.- count(*) 로 처리하고 싶으면
Wildcard.count
를 사용하면 된다. - 응답 결과는 숫자 하나이므로 fetchOne() 을 사용한다.
집합
집계 함수는 Tuple
을 반환한다.
실무에서는 Tuple 을 많이 쓰지는 않고, DTO 로 받아오는 방법으로 많이쓴다.
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);
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);
Group By
List<Tuple> result = queryFactory.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
- having() 을 통해 조건도 넣을 수 있다.
세타 조인
연관관계가 없는 필드로 조인
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
List<Member> result = queryFactory.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
assertThat(result).extracting("username")
.containsExactly("teamA", "teamB");
- from 절에 여러 엔티티를 선택해서 세타 조인
- 카테시안 곱으로 조인이 된다. (다만 DB 가 성능 최적화를 하긴함)
- 외부 조인이 불가능하다. -> 최근에는 조인 on 을 사용하면 외부 조인이 가능하다. (JPA 2.1 부터 지원)
조인 - on 절
ON 절을 활용한 조인 (JPA 2.1 부터 지원 (하이버네이트 5.1))
1. 조인 대상 필터링
참고 : on 절을 활용해 조인 대상을 필터링 할 때, 외부 조인이 아니라 내부 조인 (inner join) 을 사용하면,
where 절에서 필터링 하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때,
내부 조인이면 익숙한 where 절로 해결하고, 정말 외부 조인이 필요한 경우에만 이 기능을 사용하자.
2. 연관관계 없는 엔티티 외부 조인 (보통 이 용도로 많이 사용)
// 회원의 이름이 팀 이름과 같은 대상 외부 조인
List<Tuple> result = queryFactory.select(member, team)
.from(member)
.leftJoin(team)
.on(member.username.eq(team.name))
.fetch();
- leftJoin 부분이 연관관계가 없는 엔티티이므로 leftJoin(member.team, team) 이 아닌
leftJoin(team)
임에 유의하자.
페치 조인 (fetch join)
페치 조인은 SQL 에서 제공하는 기능은 아니다.
SQL 조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다.
주로 성능 최적화에 사용하는 방법이다.
@PersistenceUnit
EntityManagerFactory emf;
...
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(), leftJoin() 등 조인 기능 뒤에
fetchJoin()
을 추가하면 된다. - 참고 : emf.getPersistenceUnitUtil().isLoaded() 를 통해 프록시가 초기화가 되었는지 확인할 수 있다.
서브쿼리
com.querydsl.jpa.JPAExpressions
사용
// 나이가 가장 많은 회원 조회
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
from 절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리는 지원하지 않는다.
당연히 Querydsl 도 JPQL 의 빌더 역할이므로 지원하지 않는다.
하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다.
Querydsl 도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
from 절의 서브쿼리 해결방안
- 서브쿼리를 join 으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL 을 사용한다.
참고 : 화면에 맞춰서 쿼리를 짜려고 하다보면 From 절 안에 From 절 안에 From 절 ... 구조가 되기 쉽다.
DB 는 데이터를 최소화해서 가져오는 역할만 하고, 로직은 애플리케이션이나 프레젠테이션에서 처리하도록 하면
From 절의 서브 쿼리를 많이 줄일 수 있다.
한방 쿼리로 날리는게 무조건 좋을까?
실시간 트래픽이 중요한 서비스에서는 한방 쿼리로 최적화 하는게 중요할 수 있다.
하지만 어드민 페이지의 경우 한방 쿼리를 짜기위해 복잡한 쿼리로 길게 만드는 것 보다
쿼리를 나눠서 보내는게 더 나은 방식일 수 있다.
- 정말 복잡한 수천 줄의 쿼리는 쪼개서 호출하면, 몇백 줄로 줄여서 훨씬 분량을 줄일 수 있다.- SQL AntiPatterns 책에서..
Case 문
현업에서 DB는 최소한의 필터링, 그룹핑으로 데이터를 줄이는 일만해야한다.
case 로 데이터를 변환하는 작업은 DB에서 하면 안된다.
데이터 변환 작업은 애플리케이션이나 프레젠테이션 레이어에서 해야한다.
문자 더하기 concat
// 회원명_나이
List<String> result = queryFactory.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
member.age.stringValue()
: 문자가 아닌 다른 타입은 stringValue() 로 문자로 변환할 수 있다.
이 방법은 ENUM 을 처리할 때도 자주 사용된다.
참고
- 실전! Querydsl
'JPA' 카테고리의 다른 글
Querydsl - 3편 (0) | 2022.02.15 |
---|---|
Querydsl - 2편 (0) | 2022.02.08 |
JPA 를 공부하면서 알게 된 내용 정리 4 (0) | 2022.02.05 |
JPA 를 공부하면서 알게 된 내용 정리 3 (0) | 2022.02.02 |
JPA 를 공부하면서 알게 된 내용 정리 2 (0) | 2022.01.29 |
댓글