본문 바로가기
공부/Spring

[QueryDSL] 기본 문법을 알아보자

by JERO__ 2022. 8. 26.

1. JPQL vs Querydsl를 비교하여 장점을 알아보자

예제로 비교해보자. member1를 찾아보자

JPQL를 사용한 예제

String query ="select m from Member m where m.username = :username";

Member findMember = em.createQuery(query, Member.class)
        .setParameter("username", "member1")
        .getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");

Querydsl를 사용한 예제

  • JPAQueryFactory를 사용한다.
  • 컴파일 시점에 오류를 발견한다.
  • 파라미터 바인딩을 알아서 해준다.
  • 쿼리를 자바코드로 작성한다.

 

2. 기본 Q-Type을 활용하자

Q클래스 인스턴스를 사용하는 2가지 방법은 다음과 같다.

QMember qMember = new QMember("m");  //별칭 직접 지정
QMember qMember = QMember.member;    //기본 인스턴스 사용
  • 기본 인스턴스 + static import를 사용하면 위의 예제코드를 더 깔끔하게 볼 수 있다.
  • 같은 테이블을 조인해야하는 특수한 상황이 아니라면 기본 인스턴스를 사용하자
Member findMember = queryFactory
	.select(member)
	.from(member)
	.where(member.username.eq("member1"))
	.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");

 

3. 검색 조건 (where)

  • select, from을 selectFrom으로 합칠 수 있다.
  • and, or를 메서드 체인으로 연결할 수 있다.
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%’ 검색
  • and 대신 콤마(, ) 를 사용할 수도 있다.
List<Member> result1 = queryFactory
	.selectFrom(member)
	.where(member.username.eq("member1"),
				 member.age.eq(10))
	.fetch();

assertThat(result1.size()).isEqualTo(1);

 

4. 결과 조회 (마지막)

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne(), 처음 한 건 조회
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount() : count 쿼리로 변경해서 count 수 조회

 

5. 정렬 (orderBy)

예제로 알아보자!

/**
 * 회원 정렬 순서
 * 1. 회원 나이 내림차순(desc)
 * 2. 회원 이름 올림차순(asc)
 * 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
 */
List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.eq(100))
	.orderBy(member.age.desc(), member.username.asc().nullsLast())
	.fetch();

 

6. 페이징 (offset, limit)

조회 건수를 제한해보자. 다음과 같이 구할 수 있을 것이다.

List<Member> result = queryFactory
	.selectFrom(member)
	.orderBy(member.username.desc())
	.offset(1) //0부터 시작(zero index)
	.limit(2) //최대 2건 조회
	.fetch();

전체 조회 수가 필요하다면 어떻게 하면 될까? 아까 위에서 언급한 fetchResults()를 사용하면 해결할 수 있다.

QueryResults<Member> result = queryFactory
	.selectFrom(member)
	.orderBy(member.username.desc())
	.offset(1)
	.limit(2)
	.fetchResults();

assertThat(result.getTotal()).isEqualTo(4);

 

7. 집합 (groupBy, having)

집합 함수에는 어떤 것이 있을까?

  • Tuple은 Querydsl에서 제공하는 튜플임. 여러타입이 있을 때 꺼내올 수 있다.
/**
 * JPQL
 * select
	 * COUNT(m), //회원수
	 * SUM(m.age), //나이 합
	 * AVG(m.age), //평균 나이
	 * MAX(m.age), //최대 나이
	 * MIN(m.age) //최소 나이
 * from Member m
 */

List<Tuple> result = queryFactory
	.select(member.count(),
			member.age.sum(),
			member.age.avg(),
			member.age.max(),
			member.age.min())
	.from(member)
	.fetch();

assertThat(tuple.get(member.count())).isEqualTo(4);
assertThat(tuple.get(member.age.sum())).isEqualTo(100);

groupBy를 사용해보자

/**
	* 팀의 이름과 각 팀의 평균 연령을 구해라.
	*/

List<Tuple> result = queryFactory
	.select(team.name, member.age.avg())
	.from(member)
	.join(member.team, team)
	.groupBy(team.name)
	.fetch();

having을 통해 결과를 제한할 수 있다.

…
	.groupBy(item.price)
	.having(item.price.gt(1000))
…

 

8. 조인

8-1. 기본 조인

List<Member> result = queryFactory
	.selectFrom(member)
	.join(member.team, team)    // 조인대상, 별칭으로 사용할 Q타입
	.where(team.name.eq("teamA"))
	.fetch();
  • 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

8-2. 세타조인

연관관계가 없는 필드로 조인한다.

List<Member> result = queryFactory
	.select(member)
	.from(member, team)
	.where(member.username.eq(team.name))
	.fetch();
  • from 절에 여러 엔티티를 선택해서 세타조인한다.

8-3. 조인 on절을 알아보자

on절은 언제 사용될까?

두 가지 케이스가 존재한다.

  • 조인 대상을 필터링한다
  • 연관관계가 없는 엔티티 외부 조인

하나씩 살펴보자.

  1. 조인 대상을 필터링한다
@Test
public void join_on_filtering() throws Exception {
	List<Tuple> result = queryFactory
		.select(member, team)
		.from(member)
		.leftJoin(member.team, team)    // left 조인
		.on(team.name.eq("teamA"))      // on 절
		.fetch();

	for (Tuple tuple : result) {
		System.out.println("tuple = " + tuple);
	}
}

결과

t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null] // leftJoin 이기에
t=[Member(id=6, username=member4, age=40), null] // 오른쪽에 없는 경우 null
  • on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다.
  • 내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자.
  1. 연관관계가 없는 엔티티 외부 조인
/**
	* 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() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
    • 일반조인: leftJoin(member.team, team)
    • on조인: from(member).leftJoin(team).on(xxx)

8-4. 페치 조인

페치조인은 SQL에서 제공하는 기능이 아니다. SQL 조인을 활용해 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다.

Member findMember = queryFactory
	.selectFrom(member)
	.join(member.team, team).fetchJoin()   // 추가한다.
	.where(member.username.eq("member1"))
	.fetchOne();
  • 조인 기능 뒤에 fetchJoin()을 추가한다

 

9. 서브쿼리

com.querydsl.jpa.JPAExpressions 가 사용된다.

  • select 절의 예제
List<Tuple> fetch = queryFactory
	.select(member.username,
		JPAExpressions
			.select(memberSub.age.avg())
			.from(memberSub)
	).from(member)
	.fetch();
  • where절의 goe를 사용한 예제
List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.goe(
		JPAExpressions
			.select(memberSub.age.avg())
			.from(memberSub)
	))
	.fetch();
  • 위의 예제를 static import 해보자
List<Member> result = queryFactory
	.selectFrom(member)
	.where(member.age.goe(
			select(memberSub.age.avg())
				.from(memberSub)
	))
	.fetch();

한계점

  • from 절의 서브쿼리는 지원하지 않는다.

해결방안

  • 서브쿼리를 join으로 변경한다. (불가능한 상황도 있다)
  • 쿼리를 2번 분리해서 실행한다.
  • nativeSQL을 사용한다.

 

10. case문

select, where, orderBy에서 사용이 가능하다.

  • 단순한 조건 예제
List<String> result = queryFactory 
	.select(member.age
		.when(10).then("열살")
		.when(20).then("스무살")
		.otherwise("기타"))
	.from(member)
	.fetch();
  • 복잡한 조건 예제 : new CaseBuilder()
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();

11. 상수, 문자 더하기

  • 상수를 사용해보자 : Expressions.constant(xxx) 사용
Tuple result = queryFactory
	.select(member.username, Expressions.constant("A"))
	.from(member)
	.fetchFirst();
  • 문자를 더해보자
// {username}_{age}
String result = queryFactory
	.select(member.username.concat("_").concat(member.age.stringValue()))
	.from(member)
	.where(member.username.eq("member1"))
	.fetchOne();

stringValue를 통해 문자로 변환할 수 있다. 이 방법은 ENUM을 처리할 때도 자주 사용된다.

 

댓글