1. JPQL vs Querydsl를 비교하여 장점을 알아보자
예제로 비교해보자. member1를 찾아보자
JPQL를 사용한 예제
String query ="select m from Member m where m.username = :username";
Member findMember = em.createQuery(query, Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
Querydsl를 사용한 예제
- JPAQueryFactory를 사용한다.
- 컴파일 시점에 오류를 발견한다.
- 파라미터 바인딩을 알아서 해준다.
- 쿼리를 자바코드로 작성한다.
2. 기본 Q-Type을 활용하자
Q클래스 인스턴스를 사용하는 2가지 방법은 다음과 같다.
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
- 기본 인스턴스 + static import를 사용하면 위의 예제코드를 더 깔끔하게 볼 수 있다.
- 같은 테이블을 조인해야하는 특수한 상황이 아니라면 기본 인스턴스를 사용하자
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
3. 검색 조건 (where)
- select, from을 selectFrom으로 합칠 수 있다.
- and, or를 메서드 체인으로 연결할 수 있다.
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 대신 콤마(, ) 를 사용할 수도 있다.
List<Member> result1 = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"),
member.age.eq(10))
.fetch();
assertThat(result1.size()).isEqualTo(1);
4. 결과 조회 (마지막)
- fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne() : 단 건 조회
- 결과가 없으면 : null
- 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
- fetchFirst() : limit(1).fetchOne(), 처음 한 건 조회
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount() : count 쿼리로 변경해서 count 수 조회
5. 정렬 (orderBy)
예제로 알아보자!
/**
* 회원 정렬 순서
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 올림차순(asc)
* 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
*/
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
6. 페이징 (offset, limit)
조회 건수를 제한해보자. 다음과 같이 구할 수 있을 것이다.
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작(zero index)
.limit(2) //최대 2건 조회
.fetch();
전체 조회 수가 필요하다면 어떻게 하면 될까? 아까 위에서 언급한 fetchResults()를 사용하면 해결할 수 있다.
QueryResults<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetchResults();
assertThat(result.getTotal()).isEqualTo(4);
7. 집합 (groupBy, having)
집합 함수에는 어떤 것이 있을까?
- Tuple은 Querydsl에서 제공하는 튜플임. 여러타입이 있을 때 꺼내올 수 있다.
/**
* JPQL
* select
* COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이
* from Member m
*/
List<Tuple> result = queryFactory
.select(member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min())
.from(member)
.fetch();
assertThat(tuple.get(member.count())).isEqualTo(4);
assertThat(tuple.get(member.age.sum())).isEqualTo(100);
groupBy를 사용해보자
/**
* 팀의 이름과 각 팀의 평균 연령을 구해라.
*/
List<Tuple> result = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
having을 통해 결과를 제한할 수 있다.
…
.groupBy(item.price)
.having(item.price.gt(1000))
…
8. 조인
8-1. 기본 조인
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team) // 조인대상, 별칭으로 사용할 Q타입
.where(team.name.eq("teamA"))
.fetch();
- 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.
8-2. 세타조인
연관관계가 없는 필드로 조인한다.
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
- from 절에 여러 엔티티를 선택해서 세타조인한다.
8-3. 조인 on절을 알아보자
on절은 언제 사용될까?
두 가지 케이스가 존재한다.
- 조인 대상을 필터링한다
- 연관관계가 없는 엔티티 외부 조인
하나씩 살펴보자.
- 조인 대상을 필터링한다
@Test
public void join_on_filtering() throws Exception {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team) // left 조인
.on(team.name.eq("teamA")) // on 절
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
결과
t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null] // leftJoin 이기에
t=[Member(id=6, username=member4, age=40), null] // 오른쪽에 없는 경우 null
- on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.
- 내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.
- 연관관계가 없는 엔티티 외부 조인
/**
* 2. 연관관계 없는 엔티티 외부 조인
* 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
* JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
* SQL: 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"));
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("t=" + tuple);
}
}
- 문법을 잘 봐야한다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
- 일반조인: leftJoin(member.team, team)
- on조인: from(member).leftJoin(team).on(xxx)
8-4. 페치 조인
페치조인은 SQL에서 제공하는 기능이 아니다. SQL 조인을 활용해 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다.
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin() // 추가한다.
.where(member.username.eq("member1"))
.fetchOne();
- 조인 기능 뒤에 fetchJoin()을 추가한다
9. 서브쿼리
com.querydsl.jpa.JPAExpressions 가 사용된다.
- select 절의 예제
List<Tuple> fetch = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
).from(member)
.fetch();
- where절의 goe를 사용한 예제
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
- 위의 예제를 static import 해보자
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
한계점
- from 절의 서브쿼리는 지원하지 않는다.
해결방안
- 서브쿼리를 join으로 변경한다. (불가능한 상황도 있다)
- 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다.
10. case문
select, where, orderBy에서 사용이 가능하다.
- 단순한 조건 예제
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
- 복잡한 조건 예제 : new CaseBuilder()
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();
11. 상수, 문자 더하기
- 상수를 사용해보자 : Expressions.constant(xxx) 사용
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
- 문자를 더해보자
// {username}_{age}
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
stringValue를 통해 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용된다.
'공부 > Spring' 카테고리의 다른 글
[QueryDSL] 동적쿼리를 해결해보자 (0) | 2022.08.26 |
---|---|
[QueryDSL] 프로젝션(SELECT 대상)에 따라 다른 결과를 가져와 보자 (0) | 2022.08.26 |
QueryDSL을 적용해보자 (0) | 2022.08.26 |
스프링 데이터 JPA에서 Auditing을 적용해보자! (0) | 2022.08.25 |
[스프링 데이터 JPA] 사용자 정의 레포지토리를 추가로 구현하는 방법을 알아보자 (0) | 2022.08.25 |
댓글