일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 실무활용
- JPA
- QueryDSL
- API 개발 고급
- Spring Data JPA
- 타임리프 문법
- 일론머스크
- 임베디드 타입
- 벌크 연산
- 값 타입 컬렉션
- 예제 도메인 모델
- jpa 활용
- JPQL
- 검증 애노테이션
- 김영한
- 기본문법
- 스프링 mvc
- 스프링 데이터 JPA
- 로그인
- Bean Validation
- 페이징
- 트위터
- 프로젝트 환경설정
- 스프링MVC
- 타임리프
- 불변 객체
- 스프링
- JPA 활용 2
- JPA 활용2
- 컬렉션 조회 최적화
- Today
- Total
RE-Heat 개발자 일지
[Querydsl] [3] 기본 문법 (하편) 본문
실전! Querydsl - 인프런 | 강의
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔
www.inflearn.com
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
[8] 조인 - 기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할
Q 타입을 지정하면 된다.
join(조인 대상, 별칭으로 사용할 Q타입)
■ 조인 종류
- join() , innerJoin() : 내부 조인(inner join)
- leftJoin() : left 외부 조인(left outer join)
- rightJoin() : rigth 외부조인(rigth outer join)
■ 기본 조인
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() throws Exception {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
- member.team 즉 team 엔티티와 조인.
- teamA인 데이터만 추출하도록 작성
- username필드로 추출하고 예상 값이 있는지 containsExactly 메서드로 확인
- 참고] isEqualTo를 쓰면 추출한 값이 [] 안에 있어 테스트 실패가 나온다.
■ 세타 조인
/**
* 세타 조인
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@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");
}
- from 절에서 여러 엔티티를 선택해 세타 조인한다. (member, team)
- 연관관계가 설정되지 않아도 join이 가능하다.
- 카테시안 조인 (N * M)
- SQL에 cross join(상호 조인)이 명시돼 있다. 참고로 상호 조인은 한쪽 테이블의 모든 행과 다른 쪽 테이블의 모든 행을 조인시키는 기능이다.
[9] 조인 - on절
■ 조인 대상 필터링
/**
* 예시) 회원과 팀을 조인하면서 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL : select m from Member m left join team t on t.name = 'teamA'
*/
@Test
public void join_on_filtering() throws Exception {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
- 레프트 조인이므로 member 데이터는 우선 다 가져온다.
- 그러나 team은 조건이 맞지 않는 데이터는 null 상태로 가져온다.
참고] 당연히 inner조인은 조건을 충족하는 데이터만 가져온다.
@Test
public void join_on_filtering() throws Exception {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.join(member.team, team)
//.on(team.name.eq("teamA"))
.where(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
- inner 조인은 on절이나 where절이나 결과는 동일하다.
영한님 : 내부조인이면 익숙한 where절로 해결하고 외부 조인이 필요할 때만 on절을 쓰자.
■ 연관관계 없는 엔티티 외부 조인
/**
* 연관관계 없는 엔티티 외부 조인
* 회원 이름이 팀 이름과 같은 대상 외부 조인
*/
@Test
public void join_on_no_relation() throws Exception {
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))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
※ 주의사항
- 일반조인 : leftJoin(member.team, team)
- on 조인 : from(member).leftJoin(team).on(xxx)
연관관계가 없으므로 member.team이 아닌 team으로 가져온 것에 유의하자.
[10] 조인 - 페치 조인
SQL 조인을 활용해 연관된 엔티티를 SQL 한 번으로 조회하는 기능이다. N + 1 문제를 해결할 때 자주 쓰인다.
■ 페치 조인 미적용
@PersistenceUnit
EntityManagerFactory emf;
@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();
}
- 지연 로딩이라 team을 따로 조회하지 않았으므로 team이 비었다(프록시).
- 그래서 loaded가 false로 나온다.
■ 페치 조인 적용
@Test
public void fetchJoinUse() throws Exception {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1"))
.join(member.team, team).fetchJoin()
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 미적용").isTrue();
}
- 페치조인을 적용하려면 join뒤에 .fetchJoin()을 추가하면 된다.
- 페치조인으로 한 번에 가져왔으므로 team 데이터도 가져왔다. 그래서 loaded가 true값이 나온다.
[11] 서브 쿼리
쿼리 안에 쿼리 넣기! com.querydsl.jpa.JPAExpressions를 사용
■ 서브쿼리 eq 사용
/**
* 나이가 가장 많은 회원 조회
*/
@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);
}
- 서브쿼리를 쓸 때 기존 alias와는 달라야 하는 것처럼 여기서도 따로 QMember memberSub = new QMember("memberSub")를 만들었다.
- JPAExpressions를 쓰고 기존처럼 만들면 된다.
■ 서브쿼리 goe(>=) 사용
/**
* 나이가 평균 이상인 회원 조회
*/
@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);
}
■ 서브쿼리 여러 건을 처리하는 in절 사용
/**
* 나이가 10살보다 많은 회원 조회
*/
@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);
}
■ select절에 서브쿼리
@Test
public void selectSubQuery() throws Exception {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
)
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
■ from절 서브쿼리 한계
JPA JPQL은 from절 서브쿼리(인라인 뷰)는 지원하지 않는다. 따라서 Querydsl도 이를 지원하지 않는다.
해결방안
① 서브쿼리를 join으로 변경한다.
② 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
③ nativeSQL을 사용한다.
[12] Case 문
■ select, 조건절(where), order by에서 사용 가능
① 단순한 조건
@Test
public void basicCase() throws Exception {
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);
}
}
S = 열살
S = 스무살
S = 기타
S = 기타
- when : 조건문
- then : when절이 true일 때 실행
- otherwise : when절이 false일 때 실행
② 복잡한 조건
@Test
public void complexCase() throws Exception {
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();
for (String s : result) {
System.out.println("s = " + s);
}
}
- 복잡한 조건일 땐 CaseBuilder()를 사용.
■ orderBy에서 Case 문과 함께 사용하기
다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
2. 0 ~ 20살 회원 출력
3. 21 ~ 30살 회원출력
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);
}
//결과
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1
- Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select절, orderBy절과 함께 사용할 수 있다.
참고: DB에선 가급적 조건문을 주지 않아야 한다. DB에선 필요한 값만 가져오고 실제 전환하고 바꾸는 건 application 또는 프레젠테이션 레이어에서 해결하는 게 낫다.
[13] 상수, 문자 더하기
■ 상수 더하기 - Expressions.constant(xxx) 사용
@Test
public void constant() throws Exception {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
/* select
member1.username
from
Member member1 */ select
member0_.username as col_0_0_
from
member member0_
// 결과
result = [member1, A]
- JPQL에서 상수 관련 코드가 없다. 결과에서만 상수가 나타난다.
- 이유 : 위와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않는다. 대신 상수를 더하는 것처럼 최적화가 어려우면 SQL에 constant 값을 넘긴다.
■ 문자 더하기 - concat
@Test
public void concat() throws Exception {
//username_age
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
/* select
concat(concat(member1.username,
?1),
str(member1.age))
from
Member member1
where
member1.username = ?2 */ select
((member0_.username||?)||cast(member0_.age as character varying)) as col_0_0_
from
member member0_
where
member0_.username=?
- age가 int이므로 stringValue()로 문자열로 바꿔줘야 한다.
- SQL을 보면 cast돼 있는데, stringValue()를 썼기 때문이다.
- 문자열로 바꿔주는 방법은 ENUM을 처리할 때 자주 사용된다.
'백엔드 > Querydsl' 카테고리의 다른 글
[Querydsl] [4] 중급 문법 (하편) - 동적 쿼리·벌크 연산·SQL function (0) | 2023.09.30 |
---|---|
[Querydsl] [4] 중급 문법 (상편) - 프로젝션 반환·@QueryProjection (0) | 2023.09.29 |
[Querydsl] [3] 기본 문법 (상편) (0) | 2023.09.17 |
[Querydsl] [2] 예제 도메인 모델 (0) | 2023.09.15 |
[Querydsl] [1] 프로젝트 환경 설정 (0) | 2023.09.15 |