QueryDsl 검색 조건 쿼리(From, Where)
검색 조건은 username이 member1, 나이가 10~30살인 사람입니다.
select, from은 같은 경우 selectFrom으로 합쳐서 사용할 수 있습니다.
where조건은 and로 게속 이어나갈 수 있습니다.
@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1").and(member.age.between(10, 30)))
.fetchOne();
Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
}
and 로 연결이 되지만 좀 더 가독성이 좋게 만들려면 "," 를 사용해도 괜찮습니다.
@Test
public void search2() {
Member findMember = queryFactory
.selectFrom(member)
.where(
member.username.eq("member1"),
(member.age.between(10, 30))
)
.fetchOne();
Assertions.assertThat(findMember.getUsername()).isEqualTo("member1");
}
where(...) 형태이므로 매개변수로 여러개를 넣어도 and로 다 이어줄 수 있습니다.
결과조회(fetch)
- fetch()
리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne()
- 단 건 조회 결과가 없으면 : null
- 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
- fetchFirst()
limit(1).fetchOne()
- fetchResults()
페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount()
count 쿼리로 변경해서 count 수 조회
//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();
List<Member> content = results.getResults();
//count 쿼리로 변경
long count = queryFactory
.selectFrom(member)
.fetchCount();
나머지는 직관적으로 이해되지만, 페이징 쿼리를 자세히 살펴보면 총 2번의 쿼리가 나갑니다.
@Test
public void pagingQuery() {
//페이징에서 사용
QueryResults<Member> results = queryFactory
.selectFrom(member)
.fetchResults();
results.getTotal(); //첫번째 실행
List<Member> members = results.getResults(); //두번째 실행
}
첫번째는 count 쿼리이고 두번째는 Member 리스트 조회 쿼리입니다.
아래를 확인하면, 첫번째 count 쿼리는 member의 id만 확인해서 갯수를 가져옵니다.
fetchCount 쿼리도 다음과 같이 select에 칼럼을 모두 없애고 count()쿼리로 조회합니다.
@Test
public void pagingQuery() {
long count = queryFactory
.selectFrom(member)
.fetchCount();
}
성능 최적화를 위해서 fetchResults() 사용은 최소화합니다. 실무에서는 보통 count쿼리와 조회쿼리를 나눠서 2번 쿼리를 날리는게 낫습니다.
정렬
orderBy에서 정렬의 우선순위 순서대로 적어주면 됩니다. ,를 활용해 여러개를 이어붙일 수 있습니다.
1. 회원나이 내림차순
2. 회원이름 오름차순 (null은 우선순위에서 밀린다)
@Test
public void sortQuery() {
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);
Assertions.assertThat(member5.getUsername()).isEqualTo("member5");
Assertions.assertThat(member6.getUsername()).isEqualTo("member6");
Assertions.assertThat(memberNull.getUsername()).isNull();
}
아래는 JPQL 쿼리입니다. nulls last이므로 null이 마지막에 조회됩니다. nullsLast()대신에 nullsFirst()를 사용하면 null이 먼저 조회됩니다.
페이징
offset은 조회 시작 index 위치, limit은 최대 조회 갯수 제한입니다.
@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);
}
limit과 offset이 실제 쿼리 마지막에 들어간 것을 확인할 수 있습니다.
집합함수
QueryDsl의 Tuple을 사용하여 조회가 가능합니다. select에 보면 member.count(), member.age.sum(), member.age.avg(), member.age.max(), member.age.min()을 사용하여 집계함수를 사용하고, tuple.get()에서 똑같이 해당 코드를 넣으면 됩니다. 실무에서는 tuple()을 많이 사용하지 않고 dto로 바로 뽑아냅니다.
@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);
}
그룹함수
팀의 이름과 각 팀의 평균 연령을 구합니다. join()의 경우 member와 team을 inner조인합니다. groupBy로 team.name을 하기 때문에 teamA, teamB 결과가 2개 나옵니다.
@Test
public void group() throws Exception {
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);
}
member.team과 team이 team_id를 기준으로 inner join을 했으며, group by로 team.name과 member age의 평균을 묶어줍니다.
having 추가하기
groupBy로 가격별로 묶은 다음에, 가격이 1000 이상인 것만 선택하도록 합니다.
…
.groupBy(item.price)
.having(item.price.gt(1000))
…
기본 조인 사용하기
join(조인 대상, 별칭으로 사용할 Q타입)
특히 join에 두번째 인자 team은 QTeam.team을 의미합니다. 팀 A에 소속된 모든 회원을 조회합니다.
/**
* 팀 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");
}
member와 team이 team_id로 join을 하고 where에서 teamA만 걸러냅니다.
join을 leftJoin으로 바꿀 수 있으며, rightJoin도 가능합니다. 아래는 leftJoin 결과입니다.
세타조인
연관관계가 없는 2개의 테이블을 임의의 칼럼으로 조인해서 조회하는 방식입니다. 세타조인이므로 모든 결과들을 조회하고 필요한 결과들을 리턴합니다.
/**
* 세타 조인(연관관계가 없는 필드로 조인)
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@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");
}
이 세타조인은 과거에 inner join만 가능했지만 최근에 Hibernate에서 left join, right join을 지원합니다
(JPA 2.1부터 가능)
on절
on절 사용법은 크게 2가지가 있는데, 실무에서 보통 2번을 많이 사용합니다.
1. 조인 대상 필터링
2. 연관관계 없는 엔티티 외부 조인
1. 조인 대상 필터링
회원과 팀을 조인하는데, 애초에 조건이 있습니다. 모든 팀이 아니라, 팀 이름이 teamA인 팀만 조회하는 것입니다. 주석에 보다시피, SQL 문법을 보면 기본적으로 team_id는 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절에 team_id와 team name 2개를 사용하는 것을 확인할 수 있습니다.
유의 할 것은, 1.조인 대상 필터링에서 사용한 left join + on절 조건 2개 사용은 inner join + where절 사용과 완전히 같습니다. 애초에 teamA만 조인을 걸어주는 것과, 모든 팀을 조인걸고 teamA만 골라내는 것은 순서만 다를 뿐 결과가 같습니다. 실무에서는 되도록 where이 익숙한 inner join + where절을 사용해서 해결하도록 하고 정말 외부조인이 필요할 때만 1. 조인 대상 필터링을 사용합니다.
inner join + where을 사용한 방식은 아래와 같습니다.
@Test
public void join_on_filtering() throws Exception {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
2. 연관관계 없는 엔티티 외부 조인
1.조인 대상 필터링은 조인할 테이블에서 조건이 추가되었다면, 2.연관관계 없는 에티티 외부 조인은 정말 2개의 테이블이 서로 FK, PK 공유가 없어서 id로 조인이 안되는 상황입니다. 회원의 이름과 팀 이름이 같은 경우로 조회를 해봅니다.
/**
* 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);
}
}
주의할 점 2가지
1. from에서 보통 member.team으로 접근하였지만, 위에는 연관관계가 없는 막조인이 이므로 team만 사용합니다.
2. 조인대상 필터링과 달리, join의 on절에 id를 이어주는 부분이 없습니다. 단지 team name만 이어줍니다.
- 일반조인
leftJoin(member.team, team)
- on 조인
from(member).leftJoin(team).on(xxx)
fetch join
fetch join을 미적용할 때를 먼저 알아봅니다. team과 member 모두 LAZY로 조회하기 때문에 실제 조회할 때만 쿼리가 발생합니다. 아래 쿼리문에서는 member만 조회하므로 team이 전혀 load되지 않습니다. 따라서 아래의 loaded는 false가 나옵니다.
@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();
}
아래 쿼리문을 보면, member만 조회하고 team은 전혀 조회하지 않습니다.
fetch join을 사용하기 위해서는 join()쪽에 fetchJoin()을 추가하면 됩니다.
@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();
}
아래 쿼리를 보면 일반 join문법에서 fetchJoin()을 추가했을 뿐인데, member를 조회했지만, team정보도 같이 조회됩니다. LAZY 전략이기 때문에 team은 team을 조회했을 때 나와야하지만 한번에 같이 조회됩니다.
서브쿼리
서브쿼리를 위해서는 JPAExpressions를 사용하면 됩니다. 또한 서브쿼리의 member와 기존에 정의된 member와 겹치지 않기 위해서 서브쿼리용 QMember 객체를 생성해야 합니다.
/**
* 나이가 가장 많은 회원 조회
*/
@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 부분이 최대 나이로 대체된 것을 확인 할 수 있다.
서브쿼리에서 여러건을 처리하기 위해서 다음과 같이 사용할 수도 있습니다.
/**
* 서브쿼리 여러 건 처리, in 사용
*/
@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);
}
in 절 안에서 특정 나이 이상인 나이들을 조회합니다.
실행된 쿼리문은 다음과 같습니다.
select 절에서 서브쿼리 사용
select 절에서 서브쿼리를 사용합니다.
@Test
public void subQueryIn() throws Exception {
QMember memberSub = new QMember("memberSub");
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)));
}
}
쿼리 결과는 다음과 같습니다.
콘솔의 결과는 다음과 같습니다.
JPA의 한계점은 FROM에서 서브쿼리가 되지 않습니다. SELECT 절에서 서브쿼리는 Hibernate에서 지원해서 가능합니다.
from 서브쿼리 해결방법
1. 서브쿼리를 join으로 변경합니다.(불가능할 수도 있다)
2. 애플리케이션에서 쿼리를 2번 날립니다.
3. nativeSQL을 사용합니다.
보통 from절에서 서브쿼리를 많이 날리는데 잘못된 경우들이 많습니다. 예를 들어, 화면에 데이터 형식을 맞추기 위해서 db단에서부터 변환해야할 필요는 없습니다. 이때 db에서는 진짜 데이터를 where, group by쪽에서 잘 조건을 걸도록 해야합니다. 또한 한방쿼리를 만들려고 너무 애쓰지 말고 여러개로 쿼리를 날리는 고민이 필요합니다.
case문
단순한 경우, when.then()으로 조건에 맞게 선택할 수 있습니다.
@Test
public void caseQuery() throws Exception {
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
}
조금 더 복잡할 때는 CaseBuilder를 사용합니다.
@Test
public void caseQuery() 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();
}
보통 db는 raw 데이터를 조회하기 위해 사용하는 것이 좋습니다. 위의 예제같은 나이 분류는 애플리케이션에서 하는 것이 낫습니다.
상수, 문자 더하기
Expressions를 이용해 상수 A를 더합니다.
@Test
public void subQueryIn() throws Exception {
Tuple result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetchFirst();
}
특이점은 JPQL 문법에서는 상수 추가가 따로 로그에 보이지 않습니다.
@Test
public void concatQuery() throws Exception {
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
}
문자를 더하기 위해서는 concat을 사용합니다. String 리턴형을 통일시켜주기 위해서 stringValue()를 사용합니다. 또한 ENUM을 처리할 때도, stringValue()를 사용합니다.
상수더하기와 달리 concat 문법이 로그에 찍힙니다.
* 출처
Inflearn : 실전! Querydsl 강의
'Spring > Querydsl' 카테고리의 다른 글
순수 JPA와 Querydsl (0) | 2022.02.13 |
---|---|
QueryDsl 수정, 삭제 (0) | 2022.02.13 |
Querydsl 동적쿼리 (0) | 2022.02.13 |
QueryDsl 프로젝션 문법 (0) | 2022.02.13 |
JPA & JPQL & QueryDsl (0) | 2022.02.12 |