QueryDSL vs JPQL
Ex) member1 찾기
- JPQL: Stiring 형태로 쿼리를 직접 작성하고 파라미터를 세팅한다. (setParameter())
- QueryDSL: QMember라는 클래스가 있는데 이는 Member를 엔티티로 등록했기 때문에 자동으로 생성된 Q 클래스이다. QueryFactory 구문에서 FROM 절에서 테이블처럼 QMember 인스턴스를 쓸 수 있다.
- 파라미터를 바인딩 처리한다.
<hide/>
@Test
public void startJPQL() {
String qlString = "SELECT m FROM Member m "
+ "WHERE m.username = :username";
Member findMember = em.createQuery(qlString, Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQueryDSL() {
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");
}
Note)
- 공통점: EntityManager로 JPAQueryFactory 를 생성한다.
- 차이점
- JPQL: 문자 (실행 시점 오류), 파라미터 바인딩을 직접 해야한다.
- QueryDSL: 코드(컴파일 시점에 바로 오류 발생), 파라미터 바인딩을 자동으로 처리한다.
JPAQueryFactory를 필드로
Ex) 팩토리를 필드로 선언하고 member1을 찾기
- JPAQueryFactory를 필드로 제공하면 동시성 문제는?
- 동시성 문제는 JPAQueryFactory를 생성할 떄 제공하는 EntityManager에 달려있다.
- 스프링 프레임워크는 여러 스레드가 동시에 같은 EntityManager에 접근하더라도 트랜잭션마다 별도의 영속성 컨텍스트를 제공하므로 동시성 문제를 걱정하지 않아도 된다.
<hide/>
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before(){
queryFactory = new JPAQueryFactory(em);
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);;
}
@Test
public void startQueryDSL2() {
QMember m = new QMember("m");
Member findMember = queryFactory.select(m)
.from(m)
.where(m.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
기본 Q-Type 활용
클래스 인스턴스를 사용하는 방법
QMember qMember1 = new QMember("m"); // 별칭 직접 지정
QMember qMember2 = QMember.member; // 기본 인스턴스 사용
기본 인스턴스를 static import 와 함께 사용
- public static final QMember member = new QMember("member1");
- QMember 클래스에 위와 같은 static 변수가 선언되어 있다.
<hide/>
import static study.querydsl.entity.QMember.member;
@Test
public void startQueryDSL3() {
Member findMember = queryFactory.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
- 다음과같이 yml 파일에 설정해주면 JPQL 이 어떻게 실행되는지 쿼리를 볼 수 있다.
spring:
jpa:
properties:
hibernate:
use_sql_comments: true
기본 검색 쿼리
<hide/>
@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()을 메서드 체인으로 연결 가능하다.
- selectFrom() = select() + from()
- and 를 쓰지 않으면 where 절 안에 나열해서 콤마로 구분해서 조건을 넣을 수 있다.
cf) 쿼리 DSL 검색 조건 메서드
- member.username.eq("member1')
- member.username.eq("member1").not(): username != member1
- member.username.ne(): username != member1 (위와 같음)
- member.age.isNotNull(): not null인 것만 가져온다.
- member.age.in(10, 20)
- member.age.notIn()
- between()
- member.age.goe(): age >= 30
- goe(greater than or equal to): 이상
- member.age.gt(): age > 30
- gt(greater than): 초과
- member.age.loe(): age <= 30
- loe(lesser than or equal to): 이하
- member.age.lt(): age < 30
- lt(lesser than): 미만
- like("member%")
- contains("member")
- startsWith("member")
cf) Query DSL 결과 조회 메서드
- fetch(): 리스트로 결과를 반환한다. 없으면 빈 리스트 반환
- fetchOne(): 단 건을 조회하는 경우에 사용한다. 한 번 호출할 때 하나의 row만 가져온다.
- 결과 없으면 null
- 둘 이상이면: NonUniqueResultException
- fetchFirst(): 처음 한 건을 쿼리해서 가져온다.
- limit(1).fetchOne()
- fetchResults(): 해당 내용을 페이징을 위해 사용 가능하다.
- 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount(): COUNT 쿼리로 변경해서 count수를 조회한다.
- fetchAll(): 모든 데이터를 클라이언트로 한 번에 가져온다.
정렬
Ex) 정렬
<hide/>
@Test
public void sort() {
em.persist(new Member(null, 100));
em.persist(new Member("member5", 100));
em.persist(new Member("member6", 100));
List<Member> result = queryFactory.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
Member member5 = result.get(0);
Member member6 = result.get(1);
Member memberNull = result.get(2);
assertThat(member5.getUsername()).isEqualTo("member5");
assertThat(member6.getUsername()).isEqualTo("member6");
assertThat(memberNull.getUsername()).isNull();
}
- 회원 나이 내림차순, 이름 오름차순, 회원이름이 없으면 마지막에 출력되도록(nullLast()) 한다.
- nullLast(), nullFirst() 를 이용해서 null 값에 대한 순서를 부여할 수 있다.
페이징
- offset(1): 0부터 시작한다. zero index
- limit(2): 2건 조회
- queryResults.getTotal()은 페이징 조건을 적용하기 전에 모든 row()의 결과를 반환한다.
- queryResults.getResults().size(): 페이징 조건을 적용한 다음의 결과 row() 수를 반환한다.
<hide/>
@Test
public void paging() {
QueryResults<Member> queryResult = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetchResults();
assertThat(queryResult.getTotal()).isEqualTo(4);
assertThat(queryResult.getLimit()).isEqualTo(2);
assertThat(queryResult.getOffset()).isEqualTo(1);
assertThat(queryResult.getResults().size()).isEqualTo(2);
}
Note)
- 위와 같이 실행하면 count() 쿼리가 실행되므로 성능 문제에 주의한다.
- 실무에서 페이징 쿼리를 작성할 때 데이터 조회하는 쿼리는 여러 테이블을 조회하지만 count 쿼리는 조인이 필요없는 경우도 있다. 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안 나올 수 있다.
- count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면 count 전용 쿼리를 별도로 작성해야한다.
집합 함수
- COUNT(m): 회원 수
- SUM(): 나이
- AVG(): 평균
- MAX(): 최대
- MIN(): 최소
- groupBy(), having()
JOIN - 기본 조인
- join(조인 대상, 별칭으로 사용할 Q 타입)
- leftJoin()
- rightJoin()
- on()
세타 조인(theta join)
- 연관 관계가 없는 필드로 조인
- 다음과 같이 from 절 안에 매개변수로 여러 개의 엔티티를 넣어서 세타 조인할 수 있다.
- ON 절을 이용하면 조인 대상을 필터링하고 연관관계 없는 엔티티를 외부 조인할 수 있다.
<hide/>
@Test
public void theta_join() throws Exception {
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");
}
페치 조인(fetch join)
- 페치 조인은 SQL 조인을 활용해서 연관된 엔티티를 한 번에 조회하는 기능이다.
- 주로 성능 최적화를 위해 사용한다.
- 페치 조인은 SQL에서 제공하는 기능은 아니다.
Ex) 페치 조인 미적용
<hide/>
@Test
public void fetchJoinNo() throws Exception {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();
}
- 지연로딩으로 Member, Team SQL 쿼리를 각각 실행한다.
Note) 실행 결과
<hide/>
/* select member1
from Member member1
where member1.username = ?1 */ select member0_.id as id1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.username as username3_1_ from member member0_ where member0_.username=?
/* select member1
from Member member1
where member1.username = 'member1'1 */ select member0_.id as id1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.username as username3_1_ from member member0_ where member0_.username=NULL;
Ex) 페치 조인 적용
<hide/>
@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()을 추가한다.
Note) 실행 결과
<hide/>
/* select member1
from Member member1
inner join fetch member1.team as team
where member1.username = ?1 */ select member0_.id as id1_1_0_, team1_.id as id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.username as username3_1_0_, team1_.name as name2_2_1_ from member member0_ inner join team team1_ on member0_.team_id=team1_.id where member0_.username=?
/* select member1
from Member member1
inner join fetch member1.team as team
where member1.username = 'member1'1 */ select member0_.id as id1_1_0_, team1_.id as id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.username as username3_1_0_, team1_.name as name2_2_1_ from member member0_ inner join team team1_ on member0_.team_id=team1_.id where member0_.username=NULL;
서브 쿼리
- 모든 테스트는 앞에서 살펴본 before() 메서드를 실행한 다음에 실행된다는 점을 염두에 둬야한다. (@BeforeEach 애애너테이션이 있기 때문)
- JPAExpressions: 다양한 유형의 서브쿼리를 작성할 수 있는 API
- IN 서브쿼리, EXISTS 서브쿼리, FROM 서브쿼리
- 복잡한 서브쿼리를 간단하게 작성 가능하다.
Ex 1) eq , MAX 사용해서 나이가 가장 많은 멤버 조회하기
<hide/>
@Test
public void subQuery() throws Exception {
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).extracting("age")
.containsExactly(40);
}
Ex 2) goe, AVG 사용해서 나이가 평균 이상인 회원 조회하기
<hide/>
@Test
public void subQueryGoe() throws Exception {
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).extracting("age")
.containsExactly(30, 40);
}
- goe (greater or equal)
Ex 3) 여러 건 처리 in 사용
<hide/>
@Test
public void subQueryIn() throws Exception {
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).extracting("age")
.containsExactly(20, 30, 40);
}
- gt(greater than)이므로 10을 초과하는 20, 30, 40이 결과로 추출된다.
Ex) SELECT 절 안에 서브쿼리
<hide/>
QMember memberSub = new QMember("memberSub");
List<Tuple> fetch = queryFactory.select(member.username,
JPAExpressions.select(memberSub.age.avg())
.from(memberSub)
).from(member)
.fetch();
for (Tuple tuple : fetch) {
System.out.println("username = " + tuple.get(member.username));
System.out.println(
"age = " + tuple.get(JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)));
}
Note) 실행 결과
FROM 절의 서브쿼리 한계
- JPA JPQL 서브쿼리의 한계점으로 FROM 절의 서브쿼리(인라인 뷰)는 지원하지않는다.
- QueryDsl도 지원하지 않는다.
- 하이버네이트 구현체를 사용하면 SELECT 절의 서브쿼리는 지원한다.
- QueryDsl도 하이버네이트 구현체를 사용하면 SELECT 절의 서브쿼리를 지원한다.
FROM 절의 서브쿼리 해결 방안
- 서브쿼리를 JOIN으로 변경한다.
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- NativeSQL을 사용한다.
CASE
- CASE문은 SELECT, WHERE, ORDRE BY 절에서 사용 가능하다.
Ex 1) 단순 조건
- age = 10 처럼 딱 떨어지는 값일 때, 다음과 같이 쓸 수 있다.
<hide/>
List<String> result1 = queryFactory.select(
member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
Ex 2) 복잡 조건
- SELECT 절 안에 CaseBuilder를 이용해서 회원 나이 조건을 넣어준다.
<hide/>
List<String> result2 = 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();
Ex 3) OrderBy 절에서 CASE문 함께 사용하기
- 다음 우선 순위에 맞춰서 출력하라
- 0 ~ 30 살이 아닌 회원을 가장 먼저 출력
- 0 ~ 20 살 회원 출력
- 21 ~ 30 살 회원 출력
- 다음과 같이 CaseBuilder()를 이용해서 경우에 따라 순위를 나타내는 번호를 부여한다.
- List<Tuple> 에 테이블 정보와 rankPath를 같이 select 해주면 각 row에 맞는 순위 번호가 들어가고 이에 따라 내림차순 정렬된다.
- 30 세 초과인 사람이 가장 우선 순위 이므로 othrewise에 포함시킨다. 그 다음에 desc 내림차순 정렬하면 3순위가 장 먼저 출력.
<hide/>
// orderBy에서 Case문 함께 사용하기
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> result3 = queryFactory
.select(member.username, member.age, rankPath)
.from(member)
.orderBy(rankPath.desc())
.fetch();
for(Tuple tuple : result3){
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);
Note) 실행 결과
- rankPath 처럼 복잡한 조건을 변수로 선언해서 SELECT 절과 OrderBy 절에서 함께 사용 가능하다.
- 설정한 순위에 대해 내림차순 정렬을 했기 때문에 rank가 후순위인 row부터 출력된다.
<hide/>
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1
상수, 문자 더하기
Ex) 상수와 문자 이어 붙이기
- 어떤 상수를 함께 출력하기 위해 다음과 같이 Expressions.constant()를 이용할 수 있다.
<hide/>
Tuple result = queryFactory.select(member.username, Expressions.constant("A"))
.from(member)
.from(member)
.fetchFirst();
- concat()를 이용하면 문자열을 이어붙일 수 있다.
<hide/>
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
출처 - https://www.inflearn.com/course/querydsl-%EC%8B%A4%EC%A0%84
'Spring Framework > [인프런] 실전! QueryDSL' 카테고리의 다른 글
Chapter 06. Spring Data JPA가 지원하는 querydsl 기능 (0) | 2023.05.13 |
---|---|
Chapter 05. 실무 활용 - 스프링 데이터 JPA와 Querydsl (2) | 2023.05.08 |
Chapter 04. 실무 활용 - 순수 JPA와 Querydsl (1) | 2023.05.06 |
Chapter 03. 중급 문법 (0) | 2023.05.01 |
Chapter 01. QueryDsl 프로젝트 생성 및 환경 설정 (0) | 2023.04.09 |