Home Querydsl 기본 문법
Post
Cancel

Querydsl 기본 문법

기본적으로 QueryDSL에 대한 설명은 위 엔티티 모델을 기준으로 한다.

기본 Q-Type 활용

  • 우선 Querydsl을 사용하기 위해서는 EntityManager을 이용해 JPAQueryFactory를 생성해야 한다.
  • 새로운 엔티티를 만들었을 경우 gradle에서 compileJava를 해주어야 한다.
    • Q 객체 생성을 위함.

이제 기본적으로 Q-Type을 어떻게 사용해야 하는지 알아보자.

  • Q클래스 인스턴스 사용 방법
1
2
QMember qMember = new QMember("m") //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용

혹은 static import를 활용할 수도 있다.

1
2
3
Member findMember = queryFactory
		.select(QMember.member)
		//...

위와 같이도 사용할 수 있는데 이를 static import를 활용하면

1
2
3
4
5
Member findMember = queryFactory
		.select(member)
		.from(memeber)
		.where(member.username.eq("memeber1"))
		.fetchOne();

이렇게 바꿀 수 있다.

이러한 static import 방식을 제일 권장한다.

검색 조건 쿼리

우선 예시를 보자.

1
2
3
4
5
6
7
@Test  
public void search() {  
	queryFactory.selectFrom(member)  
			.where(member.username.eq("member1")  
			.and(member.age.eq(10)))  
			.fetchOne();  
}
  • 검색 조건을 .and(), .or()를 메서드 체인으로 연결해 사용할 수 있다.

정리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
member.age.notIn(10, 20)
member.age.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%")
member.username.contains("member") // like '%member%'
member.username.startsWith("member") // like 'memeber%'

헷갈리더라도 IDE를 통해 빠르게 찾을 수 있다. 자바 코드라는 장점이 여기서도 발휘된다.

And 조건을 파라미터로 처리

1
2
3
4
5
6
7
8
9
10
@Test  
public void searchAndParam() {  
	Member findMember = queryFactory.selectFrom(member)  
			.where(member.username.eq("member1"),  
					member.age.eq(10)  
			)  
			.fetchOne();  
	  
	assertThat(findMember.getUsername()).isEqualTo("member1");  
}
  • where()에 파라미터로 검색 조건을 넣으면 and()를 사용한 것과 같은 효과.
  • 이 경우 만약 null 값이 파라미터에 포함되면 null 값은 무시하기 때문에 동적 쿼리 생성에 장점이 있다.

결과 조회

  • fetch(): 리스트 조회, 데이터가 없으면 빈 리스트 반환
  • fetchOne(): 단 건 조회
    • 결과X: null
    • 결과 둘 이상이면: NonUniqueResultException 발생
  • fetchFirst(): limit(1).fetchOne()
  • fetchResults(): 페이징 정보를 포함, total count 쿼리 추가 실행
  • fetchCount(): count 쿼리로 변경해 count 수 조회
1
2
3
4
5
6
7
8
9
10
11
12
QueryResults<Member> results = queryFactory
		.selectFrom(member)
		.fetchResults();

results.getTotal()
results.getLimit()
results.getOffset()
results.getResults() //결과 데이터

long count = queryFactory
		.selectFrom(member)
		.fetchCount(); 

정렬

1
2
3
4
5
6
7
8
9
10
11
12
@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();  
}
  • 회원 나이 내림차순
  • 회원 이름 올림차순
  • 단, 회원 이름이 없다면 마지막에 출력 (nullsLast)

result의 첫 번째 결과에는 member5가 들어간다.

나이가 제일 많기 때문이다.

두 번째 결과에는 member6이 들어간다. 나이가 똑같이 제일 많지만 이름 올림차순에서 밀렸기 때문이다.

세 번째 결과에는 이름이 없는 null, age=100 Member가 들어가게 되는 것이다.

  • desc(), asc(): 일반 정렬
  • nullsLast(), nullsFirst(): null 데이터 순서 부여

페이징

1
2
3
4
5
6
7
8
9
10
11
@Test  
public void paging() {  
	List<Member> result = queryFactory  
			.selectFrom(member)  
			.orderBy(member.username.desc())  
			.offset(1) //0부터 시작(zero index)
			.limit(2) //최대 2건 조회
			.fetch();  
	  
	assertThat(result.size()).isEqualTo(2);  
}

똑같이 0부터 시작한다.

전체 조회수가 필요하다면 fetchResults()를 사용하면 된다.

참고

실무에서 페이징 쿼리를 작성할 때 데이터를 조회하는 쿼리는 여러 테이블을 조회해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있다.

그런데 fetchResults() 같은 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있다.

따라서 count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면 count 전용 쿼리를 별도로 작성해야 한다.

집합

집합 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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);  
}
  • JPQL이 제공하는 모든 집합 함수를 제공한다.
  • tuple은 Querydsl에서 제공하는 것이다.
    • 사용 방법은 위의 예제 코드처럼 사용하면 된다.
    • 보통은 DTO로 변환해 사용한다.

GroupBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//팀의 이름과 각 팀의 평균 연령을 구하라.

@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(member.age.avg())).isEqualTo(15);  
}
  • 그룹화된 결과에 조건을 걸어 제한하려면 똑같이 having을 사용하면 된다.
1
2
.groupBy(item.price)
.having(item.price.gt(1000))

조인

기본 조인

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭으로 사용할 Q타입을 지정하면 된다.

1
join(조인 대상, 별칭으로 사용할 Q타입)
1
2
3
4
5
6
7
//teamA에 소속된 모든 회원

queryFactory
		.selectFrom(member)
		.join(member.team, team)
		.where(team.name.eq("teamA"))
		.fetch();

세타 조인

연관관계가 없는 필드로 조인

1
2
3
4
5
6
7
8
9
10
11
12
// 회원의 이름이 팀 이름과 같은 회원 조회하기.

@Test
public void theta_join() {
	em.persist(new Member("teamA"));
	em.persist(new Member("teamB"));

	queryFactory
			.select(member)
			.from(member, team)
			.where(member.username.eq(team.name))
			.fetch();
  • from 절에 여러 엔티티를 선택해 세타 조인을 한다.
  • 외부 조인 불가능
    • on을 사용하면 외부 조인 가능

조인 - ON절

  • ON절을 활용한 조인(JPA 2.1 부터 지원)
    • 조인 대상 필터링
    • 연관관계 없는 엔티티 외부 조인 (주로 이 경우 많이 사용)

조인 대상 필터링

1
2
3
4
5
6
7
8
9
//회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회

@Test
public void join_on_filtering() {
	queryFactory
			.select(member, team)
			.from(member)
			.leftJoin(member.team, team).on(team.name.eq("teamA"))
			.fetch();
1
2
3
4
5
6
// 결과

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] 
t=[Member(id=6, username=member4, age=40), null]

연관관계 없는 엔티티 외부 조인

1
2
3
4
5
6
7
8
9
10
11
12
//회원의 이름과 팀의 이름이 같은 대상 외부조인

@Test
public void join_on_no_relation() {
	em.persist(new Member("teamA"));
	em.persist(new Member("teamB"));

	queryFactory
			.select(member, team)
			.from(member)
			.leftJoin(team).on(member.username.eq(team.name))
			.fetch();
  • 일반 조인과 다르게 leftJoin() 부분에 엔티티가 하나만 들어가는 것을 볼 수 있다.
1
2
3
4
5
6
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)]
  • team이름과 회원 이름이 같은 경우에만 team 정보를 가져왔다.
  • left join이기 때문에 mebmer 데이터는 전부 가져온다.

페치 조인

페치 조인은 SQL에서 제공하는 기능이 아니다. SQL 조인을 활용해 연관된 엔티티를 SQL 한 번에 조회하는 기능일 뿐이다.

1
2
3
4
5
6
7
@Test
public void fetchJoinUse() {
	queryFactory
			.selectFrom(memeber)
			.join(member.team, team).fetchJoin()
			.where(member.username.eq("member1"))
			.fetchOne();

페치조인을 사용하지 않는다면 지연 로딩으로 인해 경우에 따라 N + 1 문제가 발생할 것이다.

서브 쿼리

  • com.querydsl.jpa.JPAExpressions를 사용한다.

쿼리 내부에 쿼리를 넣는 것.

eq 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//나이가 가장 많은 회원

@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 사용

1
2
3
4
5
6
7
8
9
10
//나이가 평균 나이 이상인 회원

queryFactory  
		.selectFrom(member)  
		.where(member.age.goe(  
			JPAExpressions  
				.select(memberSub.age.avg())  
				.from(memberSub)  
		))  
		.fetch();

서브 쿼리 여러 건 처리 (in 사용)

1
2
3
4
5
6
7
8
9
queryFactory  
		.selectFrom(member)  
		.where(member.age.in(  
			JPAExpressions  
				.select(memberSub.age)  
				.from(memberSub)  
		.where(memberSub.age.gt(10))  
		))  
		.fetch();

사용 방법만 보여줄 뿐 예제 자체는 억지이다.

select절에 서브쿼리 사용

1
2
3
4
5
6
7
queryFactory
		.select(member.username,
				JPAExpressions
						.select(memberSub.age.avg())
						.from(memberSub)
		).from(member)
		.fetch();

from절의 서브 쿼리 한계

JPA JPQL 서브쿼리의 한계점으로 from절에는 서브쿼리를 지원하지 않는다.

이에 따라 Querydsl도 당연히 지원하지 않는다.

해결 방안

  • 서브쿼리를 join으로 변경한다.
  • 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
  • nativeSQL을 사용한다.

Case문

  • select, where, order by 에서 사용 가능
1
2
3
4
5
6
7
queryFactory
		.select(memeber.age
				.when(10).then("열살")
				.when(20).then("스무살")
				.otherwise("기타"))
		.from(member)
		.fetch();
1
2
3
4
5
6
7
queryFactory
		.select(new CaseBuilder()
				.when(member.age.between(0, 20)).then("0~20살")
				.when(member.age.betwwen(21, 30)).then("21~30살")
				.otherwies("기타")
		.from(member)
		.fetch();

응용

Querydsl은 자바 코드로 작성하기 때문에 보다 복잡한 조건을 변수로 선언해 select절, orderBy절에서 함께 사용할 수 있다.

  • 0~30살이 아닌 회원을 가장 먼저 출력
  • 후 0~20살 회원 출력
  • 후 21살~30살 회원 출력
1
2
3
4
5
6
7
8
9
10
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();

이런식으로 활용 가능하다.

1
2
3
4
username = member4 age = 40 rank = 3 
username = member1 age = 10 rank = 2 
username = member2 age = 20 rank = 2 
username = member3 age = 30 rank = 1

상수, 문자 더하기

상수

  • 상수가 필요한 경우 Expressions.constant(xxx) 사용
1
2
3
4
Tuple result = queryFactory
		.select(memeber.username, Expressions.constant("A"))
		.from(member)
		.fetchFirst();
1
2
3
4
5
//결과

member1, A
member2, A
member3, A

문자

1
2
3
4
5
String result = queryFactory
		.select(member.username.concat("_").concat(member.age.stringValue()))
		.from(member)
		.where(member.username.eq("member1"))
		.fetchOne();
1
2
//결과
member1_10

member.age.stringValue() 부분이 중요하다.

문자가 아닌 다른 타입들은 stringValue()로 문자로 변환이 가능하다.

이 방법은 보통 ENUM을 처리할 때 자주 사용하게 된다.

This post is licensed under CC BY 4.0 by the author.