https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/
강의를 들으며 생각 정리
시작 - JPQL vs Querydsl
지금부터 기본 문법에 대한 테스트는 다음 예제로 실행할 것이다.
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@Autowired
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
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);
}
}
EntityManager로 JPAQueryFactory를 생성한다.
JPAQueryFactory를 사용하면 EntityManager를 통해서 쿼리가 처리되고, JPQL을 사용한다.
+) JPAQueryFactory를 저렇게 필드로 제공해도 될까?
스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다. 따라서 메서드 안에서 호출할 때마다 new를 사용하는 것은 비효율적이라 권장하지 않는다.
<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", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
@Test
public void startQuerydsl() {
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1")) //파라미터 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
Querydsl은 JPQL 빌더 역할을 한다. Querydsl 코드를 보면 JPQL과 유사한 형식으로 짜여 있는 것을 볼 수 있다.
Querydsl은 Q객체를 사용한다. 인자 값으로 별칭을 사용하는데 크게 중요하지 않다.
<오류>
JPQL: 문자(실행 시점 오류)
-> JPQL string은 결국 String이기 때문에 이상하게 작성했을 때, 바로 잡아내지 못하고 런타임 오류로만 잡을 수 있다.
Querydsl: 코드(컴파일 시점 오류)
-> Querydsl은 String이 아닌 자바 형식으로 쿼리를 작성할 수 있기 때문에 컴파일 타임 때 바로 오류를 잡을 수 있다.
-> 또한 IDE가 제공하는 코드 어시스턴트 기능을 활용할 수 있어서 매우 편리하다.
<파라미터 바인딩>
JPQL: 파라미터 바인딩 직접
-> setParameter로 직접 파라미터 바인딩을 한다.
Querydsl: 파라미터 바인딩 자동 처리
-> m.username.eq("member1") 에서 쿼리가 생성될 때, where member0_.username="member1" 로 쿼리가 나가게 되면 직접 DB 값이 노출되기 때문에 Querydsl에서 자동으로 where member0_.username=? 쿼리를 남겨 자동 파라미터 바인딩을 해주게 된다.
기본 Q-Type 활용
Q클래스 인스턴스를 사용하는 2가지 방법
QMember m = new QMember("m"); //별칭 직접 지정
QMember m = QMember.member; //기본 인스턴스 사용
기본 인스턴스를 static import와 함께 사용하는 방법
import static study.querydsl.entity.QMember.*;
@Test
public void startQuerydsl() {
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
select(QMember.member)에서 QMember를 static import한 형태다.
-> 가장 편리하기 때문에 이 방법을 권장한다.
+) 다음 설정을 추가하면 Querydsl을 통해 실행되는 JPQL을 볼 수 있다.
spring.jpa.properties.hibernate.use_sql_comments: true
검색 조건 쿼리
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%’ 검색
EX) AND
@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");
}
+) 참고: select, from을 selectFrom으로 합칠 수 있다.
AND의 경우 조건 사이에 ','를 사용해도 된다.
@Test
public void searchAndParam() {
Member findMember = queryFactory
.selectFrom(member)
.where(
member.username.eq("member1"),
member.age.between(10, 30)
)
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
결과 조회
- fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne() : 단 건 조회
- 결과가 없으면 : null
- 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
- fetchFirst() : limit(1).fetchOne()
- fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount() : count 쿼리로 변경해서 count 수 조회
@Test
public void resultFetch() {
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
Member fetchOne = queryFactory
.selectFrom(QMember.member)
.fetchOne();
Member fetchFirst = queryFactory
.selectFrom(QMember.member)
.fetchFirst();
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
results.getTotal();
List<Member> content = results.getResults();
long total = queryFactory
.selectFrom(member)
.fetchCount();
}
정렬
/**
* 회원 정렬 순서
* 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()).isEqualTo(null);
}
페이징
@Test
public void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetch();
assertThat(result.size()).isEqualTo(2);
}
//전체 조회 수가 필요하다면?
@Test
public void paging2() {
QueryResults<Member> queryResults = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetchResults();
assertThat(queryResults.getTotal()).isEqualTo(4);
assertThat(queryResults.getLimit()).isEqualTo(2);
assertThat(queryResults.getOffset()).isEqualTo(1);
assertThat(queryResults.getResults().size()).isEqualTo(2);
}
+) 참고
fetchResults()의 경우 생성된 JPQL의 양식에 맞춰서 count 쿼리가 나간다.
만약 JPQL에 페치 조인이 되있는 등 복잡할 때, count 쿼리는 조인이 필요 없는 경우가 있다.
이 때는 성능 최적화를 위해 fetchResults() 보다 fetchCount()로 따로 count 쿼리를 처리하는 것이 좋다.
집합
@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);
}
JPQL이 제공하는 모든 집합 함수를 제공한다.
이렇게 여러 값을 select하는 경우 Querydsl이 제공하는 Tuple의 형태로 반환한다.
<GroupBy 사용>
/**
* 팀의 이름과 각 팀의 평균 연령을 구해라.
*/
@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("teamA");
assertThat(teamA.get(member.age.avg())).isEqualTo(15);
assertThat(teamB.get(team.name)).isEqualTo("teamB");
assertThat(teamB.get(member.age.avg())).isEqualTo(35);
}
+) having절 역시 사용 가능하다.
조인 - 기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q타입을 지정하면 된다.
-> join(조인 대상, 별칭으로 사용할 Q타입)
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
기본적으로 innerJoin을 제공하고, leftJoin, rightJoin도 지원한다.
<세타 조인>
연관관계가 없는 필드로 조인
/**
* 세타 조인
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@Test
public void theta_join() {
em.persist(new Member("teamA"));
em.persist(new Member("teamB"));
em.persist(new Member("teamC"));
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("teamA", "teamB");
}
from 절에 여러 엔티티를 선택해서 세타 조인 (내부 조인만 가능하다)
cross join이라고도 하며 한 쪽 테이블의 모든 행들과 다른 테이블의 모든 행을 조인시키는 기능을 한다.
+) 참고
Assertions의 contatinsExactly는 값의 순서도 정확해야 하기 때문에 더 정확한 테스트는 쿼리를 짤 때, 정렬도 포함시켜야 한다.
조인 - on 절
on절을 활용한 조인(JPA 2.1부터 지원)
1. 조인 대상 필터링
2. 연관관계 없는 엔티티 외부 조인
<조인 대상 필터링>
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: select m, t from Member m left join m.team t on t.name = 'teamA'
*/
@Test
public void join_on_filtering() {
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);
}
}
<결과>
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]
+) 참고
on절을 활용해 조인 대상을 필터링할 때, 외부조인이 아닌 내부조인을 사용하면 where절에서 필터링 하는 것과 기능이 동일하다. 따라서 on절을 활용한 조인 대상을 필터링할 때, 내부조인이면 익숙한 where절로 해결하고 외부조인이이면 on절을 사용하자.
<연관관계 없는 엔티티 외부 조인>
/**
* 연관관계가 없는 엔티티 외부 조인
* 회원의 이름이 팀 이름가 같은 대상 외부 조인
*/
@Test
public 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))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
<결과>
t=[Member(id=3, username=member1, age=10), null]
t=[Member(id=4, username=member2, age=20), null]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]
t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
하이버네이트 5.1부터 on을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다. 물론 내부 조인도 가능하다.
문법을 잘 봐야 한다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
- 일반조인: leftJoin(member.team, team)
- on조인: from(member).leftJoin(team).on(xxx)
정리
join()절에 member.team과 같은 경로 표현식이 들어가면 자동으로 id 매칭으로 조인해서 쿼리가 나간다.
join()절에 team과 같이 엔티티 하나만 들어가면 join에 대한 아무 조건이 없기 때문에 따로 on절을 만들어야 한다.
조인 - 페치 조인
<페치 조인 미적용>
지연로딩으로 Member는 조회했지만 연관관계인 Team은 조회되지 않는다.
@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinNo() {
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();
}
+) EntityManagerFactory로 Team이 실제 초기화 되었는지 확인할 수 있다.
<페치 조인 적용>
Member, Team을 한번에 조회한다.
@Test
public void fetchJoin() {
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();
}
join() 뒤에 fetchJoin()을 추가하면 된다.
서브 쿼리
com.querydsl.jpa.JPAExpressions 사용, 다양한 예시들을 보자.
<서브 쿼리 eq 사용>
/**
* 나이가 가장 많은 회원 조회
*/
@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);
}
+) 기존 쿼리와 서브 쿼리 간에 별칭이 중복되면 안되기 때문에 새로운 QMember 객체를 생성했다.
<서브 쿼리 goe 사용>
/**
* 나이가 평균 이상인 회원
*/
@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 사용>
/**
* 서브쿼리 여러 건 처리, 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))
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(20, 30, 40);
}
<select 절에 subquery>
@Test
public void selectSubQuery() {
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);
}
}
<static import 활용>
JPAExpression은 static import가 가능하다. static import를 통해 진짜 SQL 서브 쿼리처럼 사용할 수 있다.
import static com.querydsl.jpa.JPAExpressions.*;
@Test
public void selectSubQuery() {
QMember memberSub = new QMember("memberSub");
List<Tuple> result = queryFactory
.select(member.username,
select(memberSub.age.avg())
.from(memberSub)
)
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
<from 절의 서브쿼리 한계>
JPQ JPQL 서브쿼리는 where, select절만 지원하고 from절의 서브쿼리는 지원하지 않는다.
-> 해결 방안
1. 서브 쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다. (성능이 굉장히 중요한 상황이 아니라면 항상 한 번에 쿼리를 보내려고 애쓰지 말자)
3. 네이티브 SQL을 사용한다.
Case 문
select, where, orderBy에서 사용 가능
@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에서 Case 문>
예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
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)
.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);
}
}
Qerydsl은 자바 코드로 작성하기 때문에, rankPath처럼 복잡한 조건을 변수로 선언해서 select, orderBy절에서 함께 사용할 수 있다.
+) 참고
앞서 Case 문의 예시로서 코드를 작성해 봤는데, 사실 case 문을 통해 DB에서 직접 값을 변형해서 가져오는 것은 권장하지 않는다.
일단 DB 값을 그대로 가져오고 애플리케이션에서 case를 나누는 것을 권장한다.
상수, 문자 더하기
상수가 필요하면 Expressions.constant(xxx) 사용
@Test
public void constant() {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
단순 상수를 select 하는 경우는 최적화를 위해 JPQL에 constant 값을 내보내지 않는다.
<문자 더하기>
@Test
public void concat() {
//{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);
}
}
stringValue(): 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때 자주 사용한다.
'java > querydsl' 카테고리의 다른 글
[Querydsl] 실무 활용 - 스프링 데이터 JPA와 Querydsl (0) | 2021.06.06 |
---|---|
[Querydsl] 실무 활용 - 순수 JPA와 Querydsl (0) | 2021.06.04 |
[Querydsl] 중급 문법 (1) | 2021.06.03 |
[Querydsl] 예제 도메인 모델 (1) | 2021.06.01 |
[Querydsl] 프로젝트 환경설정 (0) | 2021.06.01 |