![[실전! Querydsl] 1. 기본 문법](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQHVWN%2FbtsDlmK2cK6%2F5Q6jz7NuOpJKmULq53AGx0%2Fimg.png)
인프런 김영한 강사님의 [실전! querydsl] 을 수강하고 정리한 글입니다.
실전! Querydsl 강의 - 인프런
Querydsl의 기초부터 실무 활용까지, 한번에 해결해보세요!, 복잡한 쿼리, 동적 쿼리는 이제 안녕! Querydsl로 자바 백엔드 기술을 단단하게. 🚩 본 강의는 로드맵 과정입니다. 본 강의는 자바 백엔
www.inflearn.com
📌 JPQL vs Querydsl
✅ JPQL과 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", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public 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");
}
Querydsl의 코드들을 자세히 들여다보자.
JPAQueryFactory를 EntityManager을 통해 생성할 수 있다.
그리고 Q라는게 엔티티 이름 앞에 붙고, 그러한 객체가 있는 것을 발견할 수 있다.
기본적으로 JPQL 느낌이 난다. Querydsl은 JPQL 빌더 라는 것을 알 수 있다.
대신, JPQL은 런타임(실행) 시점에서 오류를 잡을 수 있지만,
Querydsl은 컴파일 시점에서 오류를 잡을 수 있어 더 편리하다.
또한 JPQL은 파라미터 바인딩을 직접 해줘야 하지만,
Querydsl은 파라미터 바인딩을 자동으로 처리해준다.
참고로 JPAQueryFactory를 메서드 안이 아니라 메서드 밖, 클래스 안의 필드로서 선언해도 문제가 없다.
Spring은 여러 스레드에서 동시에 같은 EntityManager에 접근하더라도 트랜잭션 별로 영속성 컨텍스트를 각각 제공하기 때문에 동시성 문제를 걱정하지 않아도 된다.
📌 Q-Type
✅ Q 클래스 인스턴스를 사용하는 방법
QMember qMember = new QMember("m"); //새 인스턴스 생성 : 별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
위처럼 new 연산자를 활용하여 새 인스턴스를 생성하고 별칭을 직접 지정할 수도 있고,
기본 인스턴스를 활용하는 방법이 있다.
그리고 기본 인스턴스를 static import 를 통해 간편하게 생략할 수도 있다.
import static study.querydsl.entity.QMember.*;
@Test
public void startQuerydsl3() {
//member1을 찾아라.
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
위 코드에서 import static study.querydsl.entity.QMember.* 를 통해 (static import) 기본 인스턴스 방식으로 굳이 Q 클래스 인스턴스를 선언하지 않고도 사용 가능하다.
기본적으로 같은 테이블을 조인하는 경우가 아니라면 기본 인스턴스 사용을 권장한다.
그 이유는 아래와 같다.
- 간결성 및 가독성
- 일관성 : 같은 테이블에 대해 동일한 기본 인스턴스를 사용하면 일관성을 유지할 수 있다.
- 오류 감소 : 별칭을 사용하지 않음으로서 실수의 여지를 줄인다.
- 성능 : 추가적인 객체 생성 오버헤드를 피할 수 있다.
📌 검색 조건 쿼리
✅ 기본 검색 쿼리
@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");
}
기본적으로 검색 조건은 .and(), .or() 등의 메서드 체인을 통해 연결이 가능하다.
위 and 조건을 없애고 where 안에 넣음으로서 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);
}
참고로 .select()와 .from()은 .selectFrom()으로 합칠 수 있다.
✅ 검색 조건 종류
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%’ 검색
...
📌 결과 조회
✅ 결과 조회
//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();
- fetch() : 리스트 조회
- 데이터가 없다면 빈 리스트 반환
- fetchOne() : 단건 조회
- 결과가 없으면 null
- 결과가 둘 이상이면 예외 발생
- fetchFirst() : limit().fetchOne()
- fetchResults() : 페이징 정보 포함
- total count 쿼리 추가 실행
- fetchCount() : count 쿼리로 변경 후 count 수 조회
📌 정렬
✅ 정렬
/**
* 회원 정렬 순서
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 올림차순(asc)
* 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
*/
@Test
public 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();
}
.orderBy() 내부에 desc(), asc()를 통해 정렬이 가능하다.
nullsLast(), nullsFirst()는 데이터들 중 null의 순서를 부여하는 것이다.
📌 페이징
✅ 조회 건 수 제한
@Test
public void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) //0부터 시작(zero index)
.limit(2) //최대 2건 조회
.fetch();
assertThat(result.size()).isEqualTo(2);
}
.offset() : 결과 집합에서 몇 번째 행부터 데이터를 가져올 것인지 지정
.limit() : 한 번의 쿼리에서 가져올 수 있는 최대 행 수
만약 전체 조회 수가 필요하다면 fetch() 대신 .fetchResults()를 활용하면 된다.
대신 count 쿼리가 실행되니까 성능상 주의할 필요는 있다.
실무에서는 count 쿼리가 필요없을 때, 필요할 때를 구분하여 성능 최적화를 하기 위해 count 전용 쿼리를 별도 작성한다.
📌 집합
✅ 집합 함수
@Test
public void aggregation() throws Exception {
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);
}
count : 수
sum : 합
avg : 평균
max : 최대
min : 최소
tuple은 복수 컬럼을 조회할 때 사용되는 타입이다.
특히 서로 다른 엔티티에서 속성을 몇개씩 선택해야 할 경우 유용할 수 있다.
(ex> User와 Product 엔티티에서 각각의 이름과 가격을 조회할 때)
✅ GroupBy와 Having
…
.groupBy(item.price)
.having(item.price.gt(1000))
…
SQL 처럼 groupBy와 having도 지원한다.
📌 조인
✅ 기본 조인
join(조인 대상, 별칭으로 사용할 Q타입)
첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q 타입을 지정한다.
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() throws Exception {
QMember member = QMember.member;
QTeam team = QTeam.team;
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
join(), innerjoin() : 내부 조인
leftJoin() : left outer join
rightJoin() : right outer join
✅ 세타 조인
연관관계가 없는 필드끼리 조인하는 것을 말한다.
/**
* 세타 조인(연관관계가 없는 필드로 조인)
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@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 절에 여러 엔티티를 선택한다.
외부 조인이 불가능하다. (on절을 활용해야 가능하다.)
✅ on 절
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'teamA'
* SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and
t.name='teamA'
*/
@Test
public void join_on_filtering() throws Exception {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result)
System.out.println("tuple = " + tuple);
}
위처럼 on 절을 활용하여 조인 대상을 필터링할 수 있다.
이 때 외부 조인 말고 내부 조인을 사용하게 되면 where 절에서 필터링하는 것과 같은 기능을 가진다.
따라서 on절을 활용한 조인 대상 필터링에서 내부 조인이면 그냥 익숙한 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(member.team, team)
on 조인 : from(member).leftJoin(team).on(xxx)
✅ 페치 조인
페치 조인은 기본적으로 SQL에서 제공하는 기능은 아니지만 JPQL에서 제공하는 기능이다.
연관된 엔티티를 쿼리 한 번으로 모두 조회하는 기능으로, 주로 성능 최적화에 도움이 된다.
@Test
public void fetchJoinUse() throws Exception {
em.flush();
em.clear();
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).as("페치 조인 적용").isTrue();
}
fetchJoin을 사용함으로서 필요한 곳에 즉시 로딩의 효과를 볼 수 있다.
✅ 서브 쿼리
아래는 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);
}
이처럼 서브쿼리는 JPAExpressions라는 것을 활용한다.
eq 자리에 상황에 따라 goe, loe, in 등 다양한 것이 올 수 있다.
아래 select 절에 서브 쿼리를 작성한 예제이다.
List<Tuple> fetch = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
)
.from(member)
.fetch();
for (Tuple tuple : fetch) {
System.out.println("username = " + tuple.get(member.username));
System.out.println("age = " + tuple.get(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
));
}
참고로 JPAExpression을 매번 쓰기 귀찮으니 아래처럼 static import 처리를 할 수도 있다.
import static com.querydsl.jpa.JPAExpressions.select;
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
select(memberSub.age.max())
.from(memberSub)
))
.fetch();
✅ from 절의 서브 쿼리
JPA와 JPQL에서는 from 절에서의 서브 쿼리 (인라인 뷰)는 지원하지 않는다.
대신 해결방안으로는 다음과 같다.
- 서브 쿼리를 join으로 변경 (가끔 불가능할 때도 있음)
- 쿼리를 2번으로 분리하여 실행
- native query 활용
📌 Case문
✅ 예시
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();
CaseBuilder() 를 활용한다.
그 이후는 SQL 처럼 when - then의 구조다.
📌 상수, 문자 더하기
✅ 상수 더하기
상수가 필요하다면 Expressions.constant(xxx)를 활용한다.
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
✅ 문자 더하기
문자를 더하여 붙이고 싶을땐 concat()를 활용한다.
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
위처럼 문자가 아닌 것은 .stringValue() 를 활용하여 문자 타입으로 변환할 수 있다.
'Backend > JPA' 카테고리의 다른 글
[실전! Querydsl] 2. 중급 문법 (0) | 2024.01.12 |
---|---|
[실전! 스프링 데이터 JPA] 3. 확장 기능 (0) | 2024.01.10 |
[실전! 스프링 데이터 JPA] 2. 쿼리 메소드 기능 (1) | 2024.01.10 |
[실전! 스프링 데이터 JPA] 1. 공통 인터페이스 기능 (0) | 2024.01.05 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 11. 객체지향 쿼리 언어 2 (1) | 2024.01.05 |
개발자가 되고 싶어요.