JPQL 대신 Querydsl을 이용하는 이유 중 하나가 type-safe(컴파일 시점에 알 수 있는) 쿼리를 날리기 위해서 사용한다.
이 말은 JPQL에서 쿼리에 오타가 발생해도 컴파일 시점에 알기 힘들다. 오로지 런타임에서만 체크가 가능하다. 하지만 Querydsl은 컴파일 시점에 오류를 잡아줄 수 있기 때문에 좋다.
Querydsl 환경설정 검증.
엔티티 생성 후
Gradle IntelliJ 사용법
Gradle 콘솔 사용법
./gradlew clean compileQuerydsl
Q타입 생성 확인
Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
기본 인스턴스를 static import와 함께 사용하는 방법
import static study.querydsl.domain.QMember.*;
@Test
public void startQuerydsl3() {
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
application.yml에 설정 추가 → 실행되는 JPQL 볼 수 있다
spring.jpa.properties.hibernate.use_sql_comments: true
기본 검색 쿼리 - and(), or()
@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");
}
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, 230
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);
}
//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();
바로 예제부터 보면 정렬 순서는 다음과 같다.
@Test
void sort(){
//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();
Member member5 = result.get(0);
Member member6 = result.get(1);
Member memberNull = result.get(2);
//then
assertEquals("member5", member5.getUsername());
assertEquals("member6", member6.getUsername());
assertNull(memberNull.getUsername());
}
페이징 조회 개수 제한
@Test
void paging1(){
//given
//when
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.asc())
.offset(0) // 0부터 시작
.limit(3)
.fetch();
Member member1 = result.get(0);
//then
assertEquals("member1", member1.getUsername());
assertEquals(3, result.size());
}
전체 조회 수가 필요한 경우
@Test
void paging2(){
//given
//when
QueryResults<Member> queryResults = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(0)
.limit(2)
.fetchResults();
//then
assertEquals(4, queryResults.getTotal());
assertEquals(2, queryResults.getLimit());
assertEquals(0, queryResults.getOffset());
assertEquals(2, queryResults.getResults().size());
}
select.member.count()
: 멤버 숫자 구할 수 있음select.member.age.sum()
: 멤버 나이 합 구할 수 있음select.member.age.avg()
: 멤버 나이 평균 값 구할 수 있음select.member.age.max()
: 멤버 나이 맥스 구할 수 있음select.member.age.min()
: 멤버 나이 최소 구할 수 있음일반적인 집합 함수 사용 예
@Test
void aggregation(){
//given
//when
List<Tuple> result = queryFactory
.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetch();
//then
Tuple tuple = result.get(0);
assertEquals(4, tuple.get(member.count()));
assertEquals(100, tuple.get(member.age.sum()));
assertEquals(25, tuple.get(member.age.avg()));
assertEquals(40, tuple.get(member.age.max()));
assertEquals(10, tuple.get(member.age.min()));
}
select에서 내가 원하는 데이터 타입이 여러개라면 Tuple로 결과 조회. Tuple은 querydsl에서 제공하는 자료구조.
groupBy() 예제
@Test
void group(){
//given
//when
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
Tuple teamA = result.get(0);
Tuple teamB = result.get(1);
//then
assertEquals("TeamA" ,teamA.get(team.name));
assertEquals(15, teamA.get(member.age.avg()));
assertEquals("TeamB" ,teamB.get(team.name));
assertEquals(35, teamA.get(member.age.avg()));
}
having() 예제
@Test
void having(){
//given
//when
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.having(member.age.avg().gt(20))
.fetch();
Tuple teamB = result.get(0);
//then
assertEquals("TeamB", teamB.get(team.name));
assertEquals(35, teamB.get(member.age.avg()));
}
Querydsl에서 조인을 사용할 때, .join() 메서드의 첫번째 파라미터는 조인할 대상 엔티티를 지정하고, 두번째 파라미터는 별칭(alias)를 지정한다. 이 별칭은 조인된 엔티티를 참조하기 위해 사용된다.
기본조인
@Test
void join(){
//given
//when
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("TeamA"))
.fetch();
//then
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
세타조인
@Test
void thetaJoin(){
//given
em.persist(new Member("TeamA"));
em.persist(new Member("TeamB"));
//when
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
//then
Member memberA = result.get(0);
Member memberB = result.get(1);
assertEquals("TeamA", memberA.getUsername());
assertEquals("TeamB", memberB.getUsername());
}
ON 절을 활용한 조인은 조인 대상을 필터링 해주고 연관관계가 없는 엔티티는 외부 조인을 활용할 수 있다. : 조인 대상의 조건을 지정하여 필터링할 때 사용.
ON 절 예제
@Test
@DisplayName("예) 회원과 팀을 조인하면서, 팀 이름이 TeamA인 팀만 조인 하고 회원은 모두 조회한다.")
void joinOnFiltering(){
//given
//when
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team)
.on(team.name.eq("TeamA"))
.fetch();
//then
for (Tuple tuple : result) {
System.out.println(tuple);
}
}
연관관계가 없는 엔티티를 외부 조인할 경우에 ON절이 쓰인다.
@Test
@DisplayName("연관관계가 없는 엔터티 외부 조인으로 회원의 이름과 팀 이름이 같은 회원을 조인한다.")
void joinOnNoRelation(){
//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();
for (Tuple tuple : result) {
System.out.println(tuple);
}
}
leftJoin(team).on()
절을 통해서 team으로 조인을 하는데, 이 조건이 ON절에 담기게 해서 조인을 한다.패치조인은 SQL에서 제공하는 기능이 아니다. JPA에서 주로 성능 최적화를 위해서 사용하는 기능. 패치 조인을 사용하면 연관된 엔티티를 한번의 쿼리로 함께 조회할 수 있어 성능을 향상시킬 수 있음
패치 조인 미적용
@PersistenceUnit
EntityManagerFactory emf;
@Test
@DisplayName("패치조인을 사용하지 않고 데이터를 가지고 오는 방법이다.")
void fetchJoinNo(){
//given
em.flush();
em.clear();
//when
Member member = queryFactory
.selectFrom(QMember.member)
.where(QMember.member.username.eq("member1"))
.fetchOne();
//then
boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(member.getTeam());
assertEquals(false, isLoaded);
}
Hibernate:
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
where
member0_.username=?
Member(id=3, username=member1, age=10)
패치 조인 적용 예제
패치 조인을 사용하면, 연관된 엔티티를 한번의 쿼리로 함께 조회할 수 있다. → 결국 Lazy 로딩 대신 즉시 로딩(Eager Loading) 처럼 연관된 엔티티를 한 번의 쿼리로 함께 조회한다는 뜻.
@Test
@DisplayName("패치 조인을 사용해서 데이터를 가지고 오는 방법이다.")
void fetchJoinYes() {
// given
em.flush();
em.clear();
// when
Member findMember = queryFactory
.selectFrom(QMember.member)
.join(member.team, team).fetchJoin() // 패치 조인 적용
.where(QMember.member.username.eq("member1"))
.fetchOne();
// then
boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertEquals(true, isLoaded); // 연관된 팀 엔티티도 함께 로드됨
}
패치조인 동작 방식
패치 조인을 사용하면, 연관된 엔티티를 즉시 로딩하는 쿼리가 생성된다. 이는 Lazy 로딩을 우회하여 한 번의 쿼리로 필요한 데이터를 모두 가져온다.
SELECT member0_.member_id AS member_i1_0_0_,
team1_.team_id AS team_id1_1_1_,
member0_.age AS age2_0_0_,
member0_.team_id AS team_id4_0_0_,
member0_.username AS username3_0_0_,
team1_.name AS name2_1_1_
FROM member member0_
INNER JOIN team team1_
ON member0_.team_id = team1_.team_id
WHERE member0_.username = ?
이 예제에서는 member 엔티티와 team 엔티티를 패치 조인을 통해 함께 조회한다. 이를 통해 team 엔티티도 즉시 로딩되어 추가 쿼리가 발생하지 않는다.
패치 조인의 이점
패치 조인의 제약
패치 조인 사용 시기
성능 최적화를 위해 필요할 때만 연관된 엔티티를 로드하고자 할 때 사용한다. 주로 N+1 문제를 해결하기 위해 사용된다.
→ JPA에서 성능 최적화를 위해 제공되는 강력한 기능인 패치 조인은 Lazy 로딩을 우회하고 연관된 엔티티를 한 번의 쿼리로 함께 조회하여 성능을 향상시킬 수 있다. 하지만 여러 컬렉션 패치 조인 금지, 페이징 제약 등의 제약이 있다.
서브 쿼리란 select 문 안에 다시 select 문이 기술된 형태의 쿼리로 안에 있는 결과를 밖에서 받아쳐 처리하는 구조.
단일 select 문으로 조건식을 만들기가 복잡한 경우에 또는 완전히 다른 테이블에서 값을 조회해서 메인 쿼리로 이용하고자 할 때 사용한다.
서브 쿼리는 from절은 안되고, select 절이나 where절에서만 가능
JPAExpressions를 static import해서 줄이자.
SubQuery 예제 - 나이가 가장 많은 사람
@Test
@DisplayName("나이가 가장 많은 회원을 조회한다고 해보자.")
void subQuery(){
//given
QMember qMember = new QMember("m"); // alias 가 중복되면 안되므로 QMember 를 만들어줘야 한다.
//when
Member findMember = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(qMember.age.max())
.from(qMember)
))
.fetchOne();
//then
assertEquals(40, findMember.getAge());
}
SubQuery 예제 - 나이가 평균보다 같거나 큰 사람들 조회
@Test
@DisplayName("나이가 평균 이상인 회원을 조회한다고 해보자.")
void subQueryAvg(){
//given
QMember qMember = new QMember("m"); // alias 가 중복되면 안되므로 QMember 를 만들어줘야 한다.
//when
List<Member> findMembers = queryFactory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(qMember.age.avg())
.from(qMember)
))
.fetch();
//then
assertEquals(2, findMembers.size());
}
SubQuery 예제 - IN 사용
@Test
@DisplayName("나이가 10인 회원을 조회한다고 해보자. - In 을 이용")
void subQueryIn(){
//given
QMember qMember = new QMember("m"); // alias 가 중복되면 안되므로 QMember 를 만들어줘야 한다.
//when
List<Member> findMembers = queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(qMember.age)
.from(qMember)
.where(qMember.age.in(10))
))
.fetch();
//then
assertEquals(1, findMembers.size());
assertEquals(10, findMembers.get(0).getAge());
}
서브쿼리는 복잡한 조회나, 조건 적용을 위해 유용하니 잘 생각해서 사용하자.
조건에 따라서 값을 지정해주는 Case 문은 select, where, orderBy에서 사용 가능하다.
일반적인 Case 문
@Test
void baseCase(){
//given
//when
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
//then
for (String s : result) {
System.out.println(s);
}
}
조금 복잡한 Case문
@Test
void complexCase(){
//given
//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
for (String s : result){
System.out.println(s);
}
}
복잡한 Case 문
@Test
@DisplayName("0 ~ 30 살이 아닌 사람을 가장 먼저 출력하고 그 다음 0 ~ 20살, 그 다음 21살 ~ 30살 출력한다.")
void complexCase2(){
//given
NumberExpression<Integer> rankCase = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21, 30)).then(1)
.otherwise(3);
//when
List<Tuple> result = queryFactory
.select(member.username, member.age, rankCase)
.from(member)
.orderBy(rankCase.desc())
.fetch();
//then
for(Tuple tuple : result) {
System.out.println(tuple);
}
}
상수가 필요하다면 Expressions.constant()를 사용하면 된다. 줄여서 쓰고 싶다면 static import를 사용
@Test
void addConstant(){
//given
//when
List<Tuple> result = queryFactory
.select(member.username, constant("A"))
.from(member)
.fetch();
//then
for (Tuple tuple : result) {
System.out.println(tuple);
}
}
문자를 더할거면 concat을 이용하면 편하다.
@Test
void addConcat(){
//given
//when
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
//then
System.out.println(result);
}
Querydsl 동작하는 과정은 JPQL을 거쳐서 SQL로 변환되어 실행된다.