- 기초 설정
- 스프링, Querydsl 버전에 따라 설정하는 법이 계속 바뀜
- 강의 수강 시점에서의 설정 방법: https://www.inflearn.com/chats/669477
- Q파일은 gitignore
- Q타입 생성 확인 후 테스트 케이스로 실행 검증
- h2 설치 및 설정 -> sql 로그 출력, 쿼리 파라미터 로그 출력(운영 단계에서는 성능 테스트하고 사용)
- 스프링, Querydsl 버전에 따라 설정하는 법이 계속 바뀜
# application.yml
spring:
profiles:
active: local
datasource:
url: jdbc:h2:tcp://localhost/~/querydsl
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
use_sql_comments: true
logging.level:
org.hibernate.SQL: debug
# org.hibernate.type: trace
* extract String: ctrl + alt + V
JPQL vs Querydsl
- 컴파일 시점에 쿼리 코드 오류를 잡아줌
- 파라미터 바인딩 자동 처리
@Test
public void startJPQL() {
//member1 찾기
String qlString =
"select m from Member m " +
"where m.username = :username";
Member findMember = em.createQuery(qlString, Member.class)
.setParameter("username", "1")
.getSingleResult();
Assertions.assertThat(findMember.getUsername()).isEqualTo("1");
}
@Test
public void startQuerydsl() {
//member1 찾기
//JPAQueryFactory queryFactory = new JPAQueryFactory(em); // EntityManager로 JPAQueryFactory 생성
QMember m = new QMember("m");
Member findMember = queryFactory // Java 코드로 쿼리 생성 -> 컴파일 시점에서 오류 검증
.select(m)
.from(m)
.where(m.username.eq("1")) // 자동 파라미터 바인딩
.fetchOne();
Assertions.assertThat(findMember.getUsername()).isEqualTo("1");
}
- JPAQueryFactory를 필드로 두고 사용
- entity manager 자체적으로 멀티 쓰레드의 동시성 문제를 해결하도록 설계되어있어 상관 없음
- 스프링은 여러 쓰레드에서 동시에 같은 em에 접근해도 트랜잭션마다 별도의 영속성 컨텍스트를 제공하기 때문
- Q타입 활용
- 같은 테이블을 조인하는 경우에만 별칭 직접 지정하여 사용
QMember m = new QMember("m"); // 별칭 직접 지정
QMember m = QMember.member; // 기본 인스턴스 사용
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("1"))
.fetchOne();
// static import 권장!
import static study.querydsl.entity.QMember.member;
Member findMember = queryFactory // Java 코드로 쿼리 생성 -> 컴파일 시점에서 오류 검증
.select(member)
.from(member)
.where(member.username.eq("1")) // 자동 파라미터 바인딩
.fetchOne();
// jpql 쿼리문도 확인하고 싶다면 다음과 같이 설정
spring.jpa.properties.hibernate.use_sql_comments: true
검색 조건 쿼리
- where()
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("2")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("2");
}
- 검색 조건은 .and() 또는 .or()를 메서드 체인으로 연결 가능
- select, from을 selectFrom으로 합칠 수 있음
// 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, 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%’ 검색
// where()에 AND 조건을 파라미터로 처리
// 이 경우 null 값 무시 -> 동적 쿼리에 유리
@Test
public void searchAndParam() {
List<Member> result = queryFactory
.selectFrom(member)
.where(member.username.eq("2"), member.age.eq(10))
.fetch();
assertThat(result.size()).isEqualTo(1);
}
결과 조회
- fetch()
- fetchOne()
- fetchFirst()
- fetchResults()
- fetchCount()
// 리스트 조회
// 결과 없으면 빈 리스트 반환
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
// 단 건 조회
// 결과가 없으면 null 반환
// 결과가 둘 이상이면 NonUniqueResultException
Member fetchOne = queryFactory
.selectFrom(member)
.fetchOne();
// 첫 요소 조회
Member fetchFirst = queryFactory
.selectFrom(member)
.fetchFirst();
//.limit(1).fetch()
// 페이징에 사용
// count 쿼리 이후 데이터 조회 쿼리 실행 (2번)
// 복잡한 상황에서는 쿼리 따로 생성
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults(); // 결과 개수가 꼭 필요한 경우가 아니면 그냥 fetch 사용
results.getTotal(); // 개수: select 절에서 member_id만 사용
results.getResults(); // 결과 목록: select 절에서 엔티티의 모든 요소 사용
// count 쿼리로 변경
long count = queryFactory
.selectFrom(member)
.fetchCount();
정렬
- orderBy()
/**
* 회원 정렬 순서
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 올림차순(asc)
* 단, 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
*/
@Test
public void sort() {
em.persist(new Member(null, 100));
em.persist(new Member("5", 100));
em.persist(new Member("6", 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("5");
assertThat(member6.getUsername()).isEqualTo("6");
assertThat(memberNull.getUsername()).isNull();
}
페이징
- offset(), limit()
- 데이터를 조회할 때 여러 테이블을 조인하는 경우가 있는데, 같은 상황에서 count 쿼리는 조인이 필요없는 경우 성능을 위해 count 쿼리는 따로 작성하는 것이 좋다
@Test
public void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작
.limit(2) //최대 2건 조회
.fetch();
assertThat(result.size()).isEqualTo(2);
}
@Test
public void paging2() {
QueryResults<Member> queryResults = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작
.limit(2) //최대 2건 조회
.fetchResults();
assertThat(queryResults.getTotal()).isEqualTo(4);
assertThat(queryResults.getLimit()).isEqualTo(2);
assertThat(queryResults.getOffset()).isEqualTo(1);
assertThat(queryResults.getResults().size()).isEqualTo(2);
}
집합
- 집합 함수: JPQL이 제공하는 모든 집합 함수 제공
- select 절에서 가져오는 데이터들의 타입이 다양할 수 있기 때문에 반환값이 tuple 형식
/**
* JPQL
* select
* COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이
* from Member m
*/
@Test
public 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);
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);
}
* settings > Live Templates 에서 테스팅 템플릿 생성
@Test
public void $NAME$() {
//given
$END$
//when
//then
}
- group by
- group by 절에 지정된 컬럼의 값이 같은 행에 대해 집합 함수를 적용하여 계산
- 여러 컬럼을 그룹화 할 경우 해당 컬럼들의 값이 모두 같아야 같은 그룹으로 묶임
- having 절은 집합 함수를 포함하고, 그룹화된 결과에 조건을 부여
- https://keep-cool.tistory.com/37
/**
* 팀의 이름과 각 팀의 평균 연령을 구해라
*/
@Test
public void group() {
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("A");
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
assertThat(teamB.get(team.name)).isEqualTo("B");
assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
//having 예시
//아이템 가격으로 그룹화한 후 그 가격이 1000 초과되는 것만 결과로
...
.groupBy(item.price)
.having(item.price.gt(1000))
...
조인 - 기본 조인
join(조인 대상, 별칭으로 사용할 Q 타입)
- 내부 조인: join() ≡ innerJoin()
- 외부 조인: leftJoin(), rightJoin()
/**
* 팀 A에 소속된 모든 회원 조회
*/
@Test
public void join() {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("A"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("1", "2");
}
- 세타 조인: 연관관계 없는 필드로 조인
- from 절에 엔티티 나열
- 외부 조인 불가능 -> 조인 on 사용하여 해결
/**
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@Test
public void thetaJoin() {
em.persist(new Member("A"));
em.persist(new Member("B"));
List<Member> result = queryFactory
.select(member)
.from(member, team) // from 절에서 관계 없는 두 객체 나열
.where(member.username.eq(team.name))
.fetch();
//assertThat(result.get(0).getUsername()).isEqualTo("A");
//assertThat(result.get(1).getUsername()).isEqualTo("B");
assertThat(result)
.extracting("username")
.containsExactly("A", "B");
}
조인 - on 절(JPA2.1 이상)
- 조인 대상 필터링
- 외부 조인이 필요한 경우만 on 절 사용
- 내부 조인의 경우 익숙한 where 절 사용
- 예시에서는 team이 조인 대상
/**
* 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인, 회원은 모두 조회
* JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
*/
@Test
public void joinOnFiltering() {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("A"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
//결과
tuple = [Member(id=3, username=1, age=10), Team(id=1, name=A)]
tuple = [Member(id=4, username=2, age=20), Team(id=1, name=A)]
tuple = [Member(id=5, username=3, age=30), null]
tuple = [Member(id=6, username=4, age=40), null]
//참고
//내부 조인으로 바꾸면 A에 속한 멤버만 반환
...
.join(member.team, team)
.on(team.name.eq("A"))
//.where(team.name.eq("A")) //on절과 내부조인을 사용하는 것은 where로 필터링하는 것과 동일
...
//결과
tuple = [Member(id=3, username=1, age=10), Team(id=1, name=A)]
tuple = [Member(id=4, username=2, age=20), Team(id=1, name=A)]
- 연관관계 없는 엔티티 외부 조인(hibernate 5.1 이상)
/**
* 회원의 이름이 팀 이름과 같은 회원 조회 (외부 조인)
*/
@Test
public void joinOnNoRelation() {
em.persist(new Member("A"));
em.persist(new Member("B"));
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);
}
}
// 결과
// 멤버는 모두 가져오고, 조인 대상인 team은 on절의 조건에 맞는 팀만 조회
tuple = [Member(id=3, username=1, age=10), null]
tuple = [Member(id=4, username=2, age=20), null]
tuple = [Member(id=5, username=3, age=30), null]
tuple = [Member(id=6, username=4, age=40), null]
tuple = [Member(id=7, username=A, age=0), Team(id=1, name=A)]
tuple = [Member(id=8, username=B, age=0), Team(id=2, name=B)]
//일반 조인
leftJoin(member.team, team)
//on 조인
//서로 연관관게가 없기 때문에 그냥 team만 명시
leftJoin(team).on(...)
조인 - 페치 조인
- 연관된 엔티티를 SQL 한번에 조회하여 성능 최적화를 위한 기능
- join 문 뒤에 .fetchJoin()만 연결해주면 됨
@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinNo() {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 미적용").isFalse();
}
@Test
public void fetchJoinYes() {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();
}
서브 쿼리: 쿼리 속 쿼리
- JPAExpressions 사용
- where 절
/**
* 나이가 가장 많은 회원 조회
*/
@Test
public 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();
assertThat(result).extracting("age")
.containsExactly(40);
}
/**
* 나이가 평균 이상인 회원 조회
*/
@Test
public 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);
}
/**
* 여러 건 처리, in 사용
*/
@Test
public 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)) // 여러개 반환(20, 30, 40)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(20, 30, 40);
}
- select 절
- hibernate 지원
@Test
public void selectSubquery() {
QMember memberSub = new QMember("memberSub"); // 별칭 안 겹치게
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg()) // static import
.from(memberSub))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
- from 절
- JPA JPQL의 한계로 from 절에서는 서브쿼리 불가능
- 1. 서브쿼리를 join으로 변경 (불가능한 경우도 있음)
- 2. 애플리케이션 쿼리를 2번 분리해서 실행
- 3. 네이티브 SQL 사용
- 화면에서 필요한 데이터에 딱 맞게 DB에서 조회하려다 보면 너무 복잡한 쿼리가 나오는 경우가 많으므로, DB에서는 최소한의 필수적인 필터링&그루핑하고 나머지는 애플리케이션이나 프레젠테이션 단에서 해결
case 문
- 단순: when(), then(), otherwise()
- 복잡: CaseBuilder() 생성
- select 절, 조건절(where), order by에서 사용 가능
- 웬만하면 애플리케이션이나 프레젠테이션 단에서 해결하길 권장
//단순 조건
@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);
}
}
//복잡 조건
@Test
public void complexCase() {
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);
}
}
- orderBy 예제
- rankPath 처럼 복잡한 조건을 변수로 선언해서 select 절, orderBy 절에서 사용 가능
/**
* 회원 조회 우선순위
* 1. 0 ~ 30살이 아닌 회원
* 2. 0 ~ 20살인 회원
* 3. 21 ~ 30살인 회원
*/
@Test
public void OrderByCase() {
NumberExpression<Integer> rankPath = new CaseBuilder()
.when(member.age.between(0, 20)).then(2) // 2순위
.when(member.age.between(21, 30)).then(1) // 3순위
.otherwise(3); // 1순위
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);
}
}
상수, 문자 더하기
- 상수 필요할 때: Expressions.constant() 사용
- 문자 더하기: concat()
@Test
public void constant() {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A")) //SQL문에 해당 상수 안 넘어감
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
@Test
public void concat() {
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue())) // 나이는 숫자이므로 형 변환
.from(member)
.where(member.username.eq("1"))
.fetchOne();
assertThat(result).isEqualTo("1_10");
}
- stringValue()를 문자로 형변환 할 때 자주 사용하며, 특히 ENUM 처리할 때 유용하다
'course > inflearn' 카테고리의 다른 글
[실전! Querydsl] 실무 활용 - 순수 JPA와 Querydsl (0) | 2022.11.23 |
---|---|
[실전! Querydsl] 중급 문법 (0) | 2022.11.23 |
[JPA 프로그래밍 기본편] JPQL - 중급 문법 (0) | 2022.11.10 |
[JPA 프로그래밍 기본편] JPQL - 기본 문법 (0) | 2022.11.03 |
[JPA 프로그래밍 기본편] 객체지향 쿼리 언어 소개 (0) | 2022.09.19 |