Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 2024 정보처리기사 시나공 필기
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch2
- 스프링 db1 - 스프링과 문제 해결
- 스프링 mvc2 - 로그인 처리
- 스프링 고급 - 스프링 aop
- 스프링 mvc1 - 서블릿
- 타임리프 - 기본기능
- @Aspect
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch9
- 스프링 db2 - 데이터 접근 기술
- 스프링 입문(무료)
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch8
- 게시글 목록 api
- 자바의 정석 기초편 ch11
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch6
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch14
- 스프링 mvc2 - 검증
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
Archives
- Today
- Total
나구리의 개발공부기록
프로젝트 환경설정, 기본 문법 시작 - JPQL vs Querydsl, 기본 Q-Type 활용, 검색 조건 쿼리, 결과 조회, 정렬, 페이징, 집합, 조인(기본조인/on절/페치조인), 서브 쿼리, Case문, 상수 및 문자 더하기 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! Querydsl
프로젝트 환경설정, 기본 문법 시작 - JPQL vs Querydsl, 기본 Q-Type 활용, 검색 조건 쿼리, 결과 조회, 정렬, 페이징, 집합, 조인(기본조인/on절/페치조인), 서브 쿼리, Case문, 상수 및 문자 더하기
소소한나구리 2024. 10. 29. 19:37출처 : 인프런 - 실전! Querydsl (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프로젝트 환경 설정
1) 프로젝트 생성
(1) Project
- Gradle
- Java 17
- SpringBoot 3.3.5
(2) Metadata
- Group: study
- Artifact: querydsl
(3) Dependencies
- Spring Web
- Lombok
- Spring Data JPA
- H2 Database
2) Querydsl 설정, 스프링 부트 및 JPA 설정
- https://nagul2.tistory.com/318 글의 querydsl 설정 내용을 참고
3) 예제 도메인 모델
- https://nagul2.tistory.com/344 글의 도메인과 동일한 구조로 진행
- 테스트는 생략
2. 기본 문법 시작 - JPQL vs Querydsl
** 참고
- 기본적으로 JPQL, SQL을 다룰줄 안다면 거의 비슷하기 때문에 배우는데에 어렵지 않고 조금만 다뤄보면 쉽게 적응할 수 있음
1) 테스트를 위한 기본 코드
(1) 초기화 코드 작성
- 별도의 테스트 클래스에 @BeforeEach를 작성한 초기화 메서드를 작성하여 테스트 실행 전에 데이터가 입력되도록 설정
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@Autowired
EntityManager em;
@BeforeEach
void before() {
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);
}
}
2) JPQL VS Querydsl 비교 테스트
(1) 일반적인 JPQL 코드
- em.createQuery로 JPQL 코드를 작성 후 파라미터 바인딩을 이용하여 코드를 작성
- 문자이기 때문에 띄어쓰기를 주의해야함
@Test
void startJPQL() {
// member1 찾기
Member findMember = em.createQuery("select m from Member m" +
" where m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
(2) Querydsl 코드
- Querydsl을 사용하기 위해서는 JPQQueryFactory(em)으로 객체를 생성한 참조변수를 이용해야함
- 생성된 Q클래스의 객체를 생성하여 참조변수를 쿼리에 사용(만약 Q클래스생성이 안되었다면 gradle - clean, build를 해주면 생성됨)
- Querydsl은 JPQL 빌더역할이라고 보면 되는데 JPQL쿼리는 문자이기 때문에 문법오류가 발생하면 애플리케이션이 실행되고 해당 메서드가 호출되어야 문제가 발생하지만 Querydsl은 컴파일 시점에 오류를 알 수 있다는 것이 매우 큰 장점임
- 파라미터 바인딩도 직접 할 필요없이 여러가지 지원하는 메서드를 통해 값을 직접 입력해주면 자동으로 PreparedStatement로 동작함
@Test
void startQuerydsl() {
// member1 찾기
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");
}
(3) JPAQueryFactory를 필드로 이동
- JPA를 사용할 때 EntityManager처럼 JPAQueryFactory로 필드로 빼서 선언후 별도의 초기화하는 메서드로 JPAQueryFactory를 생성하도록 해도됨
- 테스트뿐아니라 리포지토리에서도 이런식으로 사용해도 괜찮음
- 멀티쓰레드에 문제없이 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에 동시성 문제 없이 잘 작동함
public class QuerydslBasicTest {
// ... 기존 코드 생략
JPAQueryFactory queryFactory; // 필드에서 선언
@BeforeEach
void before() {
queryFactory = new JPAQueryFactory(em); // JPAQueryFactory생성을 메서드로 작성
// ... 기존 코드 생략
}
}
3. 기본 Q-Type 활용
1) Q클래스 인스턴스를 사용하는 2가지 방법
QMember m = new QMember("m"); // 별칭을 직접 지정
QMember m2 = QMember.member; // QMember 생성시 생성되는 스태틱 멤버 사용
2) 기본 인스턴스를 사용한 Qeurydsl
(1) Querydsl 권장 작성 방법
- QMember의 static 변수를 static import하며 변수명으로 즉시 사용하는 것을 가장 권장함
- 실행된 JPQL 쿼리를 보면 member1라는 이름으로 필드명이 실행되는데 QMember를 빌드할 때 자동으로 이렇게 생성했기 때문임
- 별칭을 직접 입력하여 QMember의 참조변수를 생성하는 방법은 가끔 셀프 조인을 실행할 때 별칭을 다르게 사용할 때의 정도만 사용하고 일반적으로는 Q클래스의 멤버를 static import해서 직접 쿼리에 사용하도록 하자
import static study.querydsl.entity.QMember.member;
@Test
void startQuerydsl2() {
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1")) // 파라미터 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
** 참고
- Querydsl을 실행하면 SQL구문만 보이는데 application.yml설정에서 아래의 설정을 추가해주면 실행되는 JPQL을 볼 수 있음
spring.jpa.properties.hibernate.use_sql_comments: true
4. 검색 조건 쿼리
1) 검색 조건 쿼리 예시
(1) 기본 적용
- select와 From의 대상이 같은경우 selectFrom()로 한번에 조회가 가능함
- where절의 조건을 .and처럼 메서드체인으로 추가 조건을 입력할 수 있음(and뿐만 아니라 다른 기능이 많음)
// 검색 조건 쿼리
@Test
void search() {
Member findMember = queryFactory
.selectFrom(member) // select와 From의 대상이 같으면 selectFrom으로 한번에 조회가능
.where(member.username.eq("member1")
.and(member.age.eq(10))) // .and로 추가 조건을 입력할 수 있음
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
assertThat(findMember.getAge()).isEqualTo(10);
}
(2) AND 조건을 파라미터로 처리
- 조건이 and인 경우에는 .and를 생략하고 파라미터를 ,로 구분하여 조건을 입력하면 자동으로 and 조건으로 처리가 됨
- 해당 방법으로 and조건을 처리하는 경우 null값이 무시되기 때문에 동정 쿼리를 매우 깔끔하게 작성할 수 있어 조건이 and 연산만 있다면 해당 방법을 사용하는 것을 권장함
@Test
void searchParam() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"),
member.age.eq(10)
).fetchOne();
// ...
}
2) JPQL이 제공하는 주요 검색 조건
- 아래의 주요 메서드 외에도 .을 찍으면 IDE의 도움을 받아 찾아서 활용할 수 있음
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%’ 검색
...
5. 결과 조회
1) 키워드 종류
(1) 설명
- fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne() : 단건 조회, 결과가 없으면 null 결과가 둘 이상이면 NonUniqueResultException 에러 발생
- fetchFirst() : limit(1).fetchOne()
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행 - Deprecated 되었음
- fetchCount() : count 쿼리로 변경해서 count 수 조회 - Deprecated 되었음
// 결과 조회
@Test
void resultFetch() {
// 리스트로 조회, 결과 없으면 빈 리스트 반환
List<Member> MemberList = queryFactory
.selectFrom(member).fetch();
// 단건조회, 결과가 없으면 null, 2이상이면 에러 발생
Member memberOne = queryFactory
.selectFrom(member).fetchOne();
// limit(1)로 1개만 조회
Member memberFirst = queryFactory
.selectFrom(member).fetchFirst();
// 페이징 정보가 포함되어 count쿼리가 추가로 발생됨 - Deprecated
QueryResults<Member> results = queryFactory
.selectFrom(member).fetchResults();
results.getTotal();
List<Member> contents = results.getResults();
// count 쿼리로 변경 - Deprecated
long count = queryFactory.selectFrom(member).fetchCount();
}
(2) Deprecated 메서드 설명
- 스프링 부트 3.x부터는 Querydsl 5.0을 사용하는데 Deprecated된 fetchCount()와 fetchResult()는 개발자가 작성한 select 쿼리를 기반으로 count용 쿼리를 내부에서 만들어서 실행할 때 복잡한 쿼리에서는 제대로 동작하지 않는 문제가 있음
- 그래서 Querydsl에서 두 메서드를 지원하지 않기로 했기 때문에 count쿼리가 필요하다면 직접 count쿼리를 작성해서 사용하는 것을 권장함
@Test
void count() {
Long totalCount = queryFactory
//.select(Wildcard.count) //select count(*) 로 쿼리가 발생
.select(member.count()) //select count(member.id) 로 쿼리가 발생
.from(member)
.fetchOne();
System.out.println("totalCount = " + totalCount);
}
6. 정렬
1) 예제 코드
- .orderBy로 정렬 기준을 입력할 수 있음
- 파라미터로 정렬 기준을 순서대로 입력하여 여러개를 입력할 수 있음
- 예제에서는 회원 나이 내림차순, 회원 이름 오름차순으로 정렬하되 null은 마지막에 출력
- 정렬 기준에 nullsLast(), nullsFirst()로 null값이 먼저오도록 정렬할지 마지막에 오도록 정렬할지 선택할 수 있음
@Test
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();
}
7. 페이징
1) 예제 코드
- JPQL과 동일하게 offset정보와 limit 정보를 입력하면 데이터베이스 방언에따라 실행되는 SQL구문을 확인할 수 있음
- 전체 조회수가 필요할 때는 위에처럼 별도의 count 쿼리를 만들어서 사용해야함
// 페이징 - 조회 건수 제한
@Test
void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.asc())
.offset(1)
.limit(2)
.fetch();
assertThat(result.size()).isEqualTo(2);
}
8. 집합
1) 예제 코드
(1) 집계 함수들 사용
- 반환타입이 Tuple로 반환되며 꺼내는 방법은 select절에서 입력할 때와 동일함
- 실무에서는 DTO를 사용하여 반환함
- JPQL이 제공하는 모든 집합 함수를 제공함
- select절에 추가 대상을 지정해야하므로 select와 from을 따로 작성해야함
// 집합 테스트 - 반환타입이 Querydsl의 Tuple로 꺼내짐(실무에서는 DTO로 꺼냄)
@Test
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 tuple = result.get(0);
// select 절에서 사용한 함수와 동일한 이름으로 값을 꺼낼 수 있음
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);
}
(2) GroupBy 사용
- JPQL과 동일한 문법으로 사용가능하며 Group화된 결과에 조건을 적용하기위한 having도 사용 가능함
/**
* 팀의 이름과 각 팀의 평균 연령을 구하기
*/
@Test
void groupBy() {
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);
assertThat(teamA.get(team.name)).isEqualTo("teamA");
assertThat(teamA.get(member.age.avg())).isEqualTo(15); // 30 / 2
assertThat(teamB.get(team.name)).isEqualTo("teamB");
assertThat(teamB.get(member.age.avg())).isEqualTo(35); // 70 / 2
}
9. 조인 - 기본 조인
1) 예제 코드
(1) 기본 조인
- join()으로 조건을 입력하면 inner join으로 쿼리가 발생함
- leftjoin(), rightjoin()도 적용할 수 있으며 inner join과 마찬가지로 일반적인 JPQL, SQL문법과 동일하게 사용하고 동일하게 쿼리가 발생함
- on절을 추가로 입력하여 join 조건을 입력할 수 있음
// 기본 Join - teamA에 소속된 모든 회원
@Test
void join() {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result).extracting("username") // username의 필드를 검증
.containsExactly("member1", "member2"); // 검증할 값을 입력
}
(2) 세타조인
- 연관관계가 없는 엔터티끼리도 from절에 엔터티를 입력하여 세타 조인을 할 수 있음
- 세타 조인은 일반적으로 외부 조인이 불가능하며 on절을 이용해야 외부 조인이 가능함
- 세타 조인은 카르테시안 곱(교차곱)으로 쿼리가 수행되며 DB마다 차이가 있지만 대부분 내부적으로 최적화가 동작하여 실행됨
@Test
void theta_join() {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
List<Member> result = queryFactory
.select(member)
.from(member, team) // from절에 join할 엔터티를 입력하면 join이 가능함
.where(member.username.eq(team.name))
.fetch();
assertThat(result).extracting("username").containsExactly("teamA", "teamB");
}
10. 조인 - on절
** JPA 2.1부터 지원
1) 조인 대상 필터링
(1) 회원과 팀을 조인하면서 팀 이름이 teamA인 팀만 조인하고 회원은 모두 조회
- on절에 left join 조건을 입력하여 회원이름은 전부 출력하고 team 정보는 teamA의 값만 입력됨
- left join이 동작하여 on절 조건이 맞지않은 값들은 null이 입력됨
** 참고
- on절을 활용해 조인 대상을 필터링할 때 외부조인이 아닌 내부 조인을 사용하면 where절에서 필터링하는 것과 동일한 기능임
- 내부조인인 경우 익숙한 where문으로 필터링하고 외부조인이 필요한 경우 on절을 이용하는 것을 권장함
/**
* on절 활용 - 조인대상 필터링
* 회원과 팀을 join, 팀 이름이 teamA인 팀만 조인하고 회원은 모두 조회
* JPQL : select m, t from Member m left join m.team t on t.name = 'teamA'
*/
@Test
void join_on_filtering() {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
// inner조인일 경우 on이나 where나 동일한 기능을 하게됨
// .join(member.team, team).where(team.name.eq("teamA"))
// .join(member.team, team).on(team.name.eq("teamA"))
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
/* 출력 결과
tuple = [Member(id=1, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=2, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=3, username=member3, age=30), null]
tuple = [Member(id=4, username=member4, age=40), null]
*/
2) 연관관계 없는 엔터티 외부 조인
(1) 회원의 이름과 팀의 이름이 같은 대상을 외부 조인
- leftJoin()에 연관관계가 없는 엔터티를 입력하고 on절에 join 조건을 입력하여 연관관계가 없는 엔터티를 외부조인 할 수 있음
- 출력결과를 보면 Member의 데이터는 전부 출력되고 on절의 조건에 부합되는 Team이름만 출력되고 나머지는 null로 출력되어 left Join이 정상적으로 이루어진 것을 확인할 수 있음
- 실행된 SQL과 JPQL도 정상적으로 Left Join 쿼리를 전송함
/**
* on절 활용 - 연관관계 없는 엔터티 외부 조인
* 회원의 이름과 팀의 이름이 같은 대상 외부 조인
* 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
void join_on_no_relation() {
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)) // 연관관계가 없는 team을 leftjoin
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
/* 출력 결과
tuple = [Member(id=1, username=member1, age=10), null]
tuple = [Member(id=2, username=member2, age=20), null]
tuple = [Member(id=3, username=member3, age=30), null]
tuple = [Member(id=4, username=member4, age=40), null]
tuple = [Member(id=5, username=teamA, age=0), Team(id=1, name=teamA)]
tuple = [Member(id=6, username=teamB, age=0), Team(id=2, name=teamB)]
tuple = [Member(id=7, username=teamC, age=0), null]
*/
11. 조인 - 페치조인
1) 설명
- SQL에서 제공하는 기능이아닌 SQL조인을 활용하여 연관된 엔터티를 SQL한번에 조회하는 기능으로 N + 1 문제를 해결하기위한 성능 최적화에 사용하는 방법임
2) 예제 코드
(1) 페치 조인 미적용
- Member만 조회하면 당연히 Member만 조회하게되고 Team의 정보는 영속성 컨텍스트에 로딩되어있지 않음
- EntityManagerFactory의 getPersistenceUnitUtil() 메서드로 인자의 대상이 영속화 되었는지의 여부를 확인할 수 있음
- 이 상태에서 조회된 findMember에서 Team의 필드에 접근하는 순간 지연로딩이 적용되어 select 쿼리가 발생됨(N+1문제, JPA강의 및 JPA 활용편에서 엄청 강조한 내용)
@PersistenceUnit
private EntityManagerFactory emf;
// 페치조인 미적용
@Test
void fetchJoinNo() {
// 영속성컨텍스트를 비워서 처음부터 시작하도록 설정
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"))
.fetchOne();
// 영속성 컨텍스트에 조회결과인 findMember의 안에 Team이 로딩 되었는지 확인 - fetch join을 적용안했으므로 false가 나와야함
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();
String teamName = findMember.getTeam().getName();
boolean loadedTeam = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loadedTeam).as("team 로드").isTrue();
}
(2) 페치 조인 적용
- join을 작성할 때 메서드 체인으로 fetchJoin()을 적용하면 Team의 정보가 영속성 컨텍스트에 저장되어있음
- 발생되는 쿼리를 보면 join 쿼리가 발생되어 한번에 연관된 Team의 정보를 가져오는 쿼리가 발생된 것을 확인할 수 있음
// 페치조인 적용
@Test
void fetchJoinUse() {
// 영속성컨텍스트를 비워서 처음부터 시작하도록 설정
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin() // 페치 조인 적용
.where(member.username.eq("member1"))
.fetchOne();
// 페치 조인을 적용했으므로 Team의 정보가 이미 영속화 되어있음
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();
}
12. 서브쿼리
1) 예제 쿼리
- JPAExpressions를 사용하여 서브쿼리를 입력할 수 있으며 Static Import 적용이 가능함
(1) 나이가 가장 많은 회원 조회 - 동등 연산 사용
- 같은 엔터티로 select 절에 서브쿼리가 들어가기 때문에 같은 별칭을 사용할 수 없어 새로운 별칭으로 참조변수를 생성하여 서브쿼리를 작성하면됨
- where절의 eq( = )의 조건에 서브쿼리를 입력
// 서브쿼리1 - 나이가 가장 많은 회원 조회
@Test
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();
// result의 age가 40
assertThat(result).extracting("age").containsExactly(40);
}
(2) 나이가 평균 이상인 회원 - 비교연산 사용
- where절의 goe(>=) 조건에 서브쿼리 작성
// 서브쿼리2 - 나이가 평균 이상인 회원
@Test
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).extracting("age").containsExactly(30, 40);
}
(3) 나이가 10을 초과하는 회원 조회 - in 사용
- where절의 in 조건에 서브쿼리 작성
- 서브쿼리의 gt는 > 연산을 뜻함
// 서브쿼리3 - age가 10 초과인 회원 조회
@Test
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)) // gt (>)
))
.fetch();
assertThat(result).extracting("age").containsExactly(20, 30, 40);
}
(4) select 절에 서브쿼리를 적용
- JPAExpressions static import 적용한 예제
- select 절에 서브쿼리를 적용하여 age의 평균값을 모든 member.username과 함께 반환
// 서브쿼리4 - select절에 서브쿼리
@Test
void selectSubQuery() {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg()) // age의 평균값 반환
.from(memberSub))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
assertThat(tuple.get(1, Double.class)).isEqualTo(25.0);
}
** 참고
- 기존에는 from절 서브쿼리(인라인 뷰)를 지원하지 않았지만 하이버네이트 6.1부터 from절 서브쿼리를 지원하기 시작하였으나 Querydsl에서는 아직까지는 공식적으로 지원하고 있지는 않아 서드파티를 적용해야 함
- 인라인뷰의 사용은 항상 성능 문제의 이슈가 있어 사용할 때 쿼리를 분리하는 것을 고려해야 할 수도 있음
12. Case문
- JPQL에서 지원하는 case문의 코드 모두 지원
- 그러나 꼭 DB에서 이런 변환하는 기능 사용해야하는지에 대해서는 고민이 필요함
- DB에서는 최소한의 필터링, 그룹설정하는등의 계산을 할 수는 있지만 이렇게 DB의 값을 전환하거나 변환하는 작업은 애플리케이션에서 하는 것을 권장함
1) 예제 코드
(1) 단순한 조건
- 단순하게 값을입력하는 case문
// 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);
}
}
(2) 복잡한 조건
- CaseBuilder()를 사용하여 when절에 조건을 입력할 수 있음
// case 문 - 복잡한 조건
@Test
public void complexCase() {
List<String> result = queryFactory
.select(new CaseBuilder()
.when(member.age.between(0, 20)).then("20세 이하")
.when(member.age.between(21, 30)).then("21 ~ 30세")
.otherwise("31세 이상"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
(3) orderBy 에서 case문 함께 사용
- new CaseBuilder()의 조건을 숫자로 지정하여 orderBy를 이용하여 조건으로 출력 순서를 지정할 수 있음
// case 문 - orderBy와 함께 사용
@Test
public void orderByCase() {
// CaseBuilder의 각 조건의 결과를 숫자로 지정
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);
}
}
13. 상수, 문자 더하기
1) 상수가 필요할 때
- Expressions.constant()를 사용하여 상수의 값을 입력할 수 있음
- 실행해보면 SQL tuple에 상수값이 같이 나오지만 실행된 SQL이나 JPQL을 보면 상수가 보이질 않는데 그이유는 내부적으로 최적화가 적용되어 성능을 높이기 때문임
- 만약 Querydsl쿼리에서 복잡한 상수작업이 필요하여 최적화를 못하게 되면 SQL쿼리에 상수를 직접 넘김
// 상수가 필요할 때
@Test
void constant() {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
2) 문자 더하기
- SQL의 concat()함수와 동일
- 문자가 아닌 다른 타입은 .stringValue()를 통해 문자로 변환해줘야하는데 ENUM을 처리할 때 자주 사용함
// 문자 더하기, concat
@Test
public void concat() {
// username_age 처럼 적용해보기
List<String> result = queryFactory
// age는 숫자기이 때문에 .stringValue()로 타입 캐스팅을 해줘야함(ENUM 처리할때 자주 씀)
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}