Skip to content

기본문법

엔티티 조회

@Test
public void searchWithQuerydsl() {
//given
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
//when
//then
assertEquals("member1", findMember.getUsername());
}

Querydsl에서는 Q클래스 인스턴스를 사용해 쿼리를 작성할 수 있다.

기본 인스턴스를 static import하면 편리하게 사용할 수 있다.

application.yml에 다음 설정을 추가하면 실행되는 JPQL을 볼 수 있다.

spring.jpa.properties.hibernate.use_sql_comments: true

검색 조건

@Test
public void search() {
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}

BooleanExpression을 파라미터로 넣어 Where절에 조건을 줄 수 있다. and(),or() 메서드를 체인으로 연결하는 것도 가능하다.

@Test
public void search() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("member1") ,member.age.eq(10))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}

where 절에 콤마(,)로 구분하여 여러 파라미터를 넣으면 and 조건이 추가된다.
또,select()from()값이 같은 경우 selectFrom()를 사용할 수 있다.

Querydsl은 JPQL이 제공하는 모든 검색조건을 제공한다.

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() //username 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, 230
member.age.goe(30) // greater or equa,l age >= 30
member.age.gt(30) // greater, age > 30
member.age.loe(30) // lower or equal, age <= 30
member.age.lt(30) // lower, age < 30
member.username.like("member%") // like 검색
member.username.contains("member") // like %member% 검색
member.username.startswith("member") // like member% 검색

결과 조회

fetch 명령어

//List
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
//단 건
Member fetchOne = queryFactory
.selectFrom(member)
.fetchOne();
//처음 한 건 조회
Member fetchFirst = queryFactory
.selectFrom(member)
.fetchFirst();

fetch() 메소드로 리스트를 조회할 수 있다. 데이터가 없으면 빈 리스트가 조회된다.

fetchOne()은 한건의 결과를 조회한다. 데이터가 없으면 Null이 조회되고, 결과가 둘 이상이면 NonUniqueResultException이 발생한다.

fetchFirst()는 첫번쨰로 조회되는 결과를 조회한다. 결과가 여러개인 경우에도 하나만 반환한다. limit(1).fetchOne()과 동일하다.


정렬

OrderBy 예시

@Test
public void sort() {
//given
em.persist(new Member(null, 100));
em.persist(new Member("member5", 100));
em.persist(new Member("member6", 100));
//when
List<Member> results = queryFactory
.selectFrom(member)
.where(member.age.goe(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
Member member5 = results.get(0);
Member member6 = results.get(1);
Member memberNull = results.get(2);
//then
assertEquals("member5", member5.getUsername());
assertEquals("member6", member6.getUsername());
assertNull(memberNull.getUsername());
}

orderBy() 를 통해서 정렬을 시작할 수 있다. desc()를 사용하면 내림차순으로, asc()를 사용하면 오름차순으로 정렬할 수 있다.

지정하지 않을 경우 오름차순이 기본 설정이다.

nullLast()nullFirst()로 null 데이터에 순서를 부여할 수 있다.


집계 함수

count, sum, avg, min, max등의 집계 함수가 들어간 쿼리도 작성할 수 있다.

집계함수 예시

@Test
void aggregation(){
//given
//when
List<Tuple> results = queryFactory
.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min()
)
.from(member)
.fetch();
//then
Tuple tuple = results.get(0);
assertEquals(4, tuple.get(member.count()));
assertEquals(100, tuple.get(member.age.sum()));
assertEquals(25, tuple.get(member.age.avg()));
assertEquals(40, tuple.get(member.age.max()));
assertEquals(10, tuple.get(member.age.min()));
}

해당 집계 함수의 기능대로 잘 동작하는 것을 볼 수 있다.

여기서 추가로, select 에서 내가 원하는 데이터를 타입이 여러개인 경우에는 Tuple 로 결과를 조회할 수 있고, get()으로 데이터를 가져올 수 있다.

하지만 실무에서는 Tuple로 뽑기보다는 DTO를 사용하는 경우가 많다.


GroupBy, Having 절

GroupBy절 예시

@Test
public void group() {
//given
//when
List<Tuple> results = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.fetch();
Tuple resultA = results.get(0);
Tuple resultB = results.get(1);
//then
assertEquals("TeamA", resultA.get(team.name));
assertEquals(15, resultA.get(member.age.avg()));
assertEquals("TeamB", resultB.get(team.name));
assertEquals(35, resultB.get(member.age.avg()));
}

Having절 예시

@Test
public void having() {
//given
//when
List<Tuple> results = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name)
.having(member.age.avg().gt(20))
.fetch();
Tuple teamB = results.get(0);
//then
assertEquals("TeamB", teamB.get(team.name));
assertEquals(35, teamB.get(member.age.avg()));
}

조인과 ON절

Join절 예시

@Test
public void join() {
//given
//when
List<Member> results = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("TeamA"))
.fetch();
//then
assertThat(results)
.extracting("username")
.containsExactly("member1", "member2");
}

join()을 사용하게 되면 기본적으로 inner join이 실행된다.

leftJoin()rightJoin()도 동일한 방법으로 사용하면 된다.

On절

@Test
void join_On(){
//given
//when
List<Tuple> results = queryFactory
.select(member, team)
.from(member)
.leftJoin(member.team, team)
.on(team.name.eq("TeamA"))
.fetch();
List<String> teamAList = results
.stream()
.filter(tuple -> tuple.get(team) != null && Objects.equals(tuple.get(team).getName(), "TeamA"))
.map(tuple -> tuple.get(member).getUsername())
.collect(Collectors.toList());
List<String> teamBList = results
.stream()
.filter(tuple -> tuple.get(team) == null)
.map(tuple -> tuple.get(member).getUsername())
.collect(Collectors.toList());
//then
assertThat(teamAList).contains("member1", "member2");
assertThat(teamBList).contains("member3", "member4");
}

On절도 사용할 수 있다.


세타조인

세타조인은 연관관계가 없는 테이블을 조인할때 사용할 수 있는 방법 중 하나이다. 세타조인은 교차조인으로 두 테이블의 카테시안 곱을 만든 뒤, =, <, > 등의 비교 연산자로 구성된 조건을 만족하는 튜플을 선택하여 반환하고, 비교연산자가 =인 경우에는 동등 조인(equi join)이라고 부르기도 한다.

즉, 두 테이블에 튜플을 하나하나, 모든 경우의 수를 다 조합하여 비교한 결과를 반환하는 것이다. 조건을 안넣으면 교차조인(Cross Join)이라고 생각할 수 있다.

세타조인 예시

@Test
public void thetaJoin() {
//given
em.persist(new Member("m1",20));
em.persist(new Member("m2",20));
//when
List<Member> results = queryFactory
.select(member)
.from(member, team)
.where(member.username.length().lt(team.name.length()))
.fetch();
//then
Member memberA = results.get(0);
Member memberB = results.get(1);
assertEquals("m1", memberA.getUsername());
assertEquals("m1", memberB.getUsername());
}
--Cross Join 쿼리가 나가는 모습이다.
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_ cross
join
team team1_
where
length(member0_.username)<length(team1_.name)

페치 조인

페치 조인은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하여 성능을 최적화하는 기능이다.

페치 조인 예시

@Test
public void fetchJoin() {
//given
em.flush();
em.clear();
//when
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team)
.fetchJoin()
.where(member.username.eq("member1"))
.fetchOne();
//then
boolean isLoaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertTrue(isLoaded);
}

Member에서 Team에 joinFetchType.LAZY가 설정되어있기 때문에 Member의 정보만 select한 경우에는 Team이 load되지 않은 상태여야 하는데, fetchJoin()를 사용하여 연관된 Team을 한번에 가져오도록 설정했기 때문에 테스트가 성공하는 것을 볼 수 있다.

@Entity
public class Member {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
...
}

실제 쿼리

-- Fetch Join인 경우 실행되는 쿼리
select
member0_.member_id as member_i1_0_0_,
team1_.team_id as team_id1_1_1_,
member0_.age as age2_0_0_,
member0_.team_id as team_id4_0_0_,
member0_.username as username3_0_0_,
team1_.member_id as member_i3_1_1_,
team1_.name as name2_1_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
team1_.name=?
-- Fetch Join가 아닌 경우 실행되는 쿼리
select
member0_.member_id as member_i1_0_,
member0_.age as age2_0_,
member0_.team_id as team_id4_0_,
member0_.username as username3_0_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.username=?

서브 쿼리

서브 쿼리란 SELECT 문 안에 다시 SELECT 문이 기술된 형태의 쿼리로, 안에 있는 쿼리의 결과를 조회한 후에 그 결과로 메인 쿼리가 실행되는 구조를 가지고있다. 단일 SELECT 문으로 조건식을 만들기가 복잡한 경우, 또는 완전히 다른 별개의 테이블에서 값을 조회하여 메인쿼리로 이용하고자 하는 경우에 사용된다.

서브쿼리는 주로 Where절에서 사용되고, Select절에서도 사용할 수 있다. Querydsl-jpa에서 From절은 지원되지 않는다.

같은 테이블을 두번 사용하기 때문에 별도의 별칭(alias)이 필요하여 m이라는 앨리어스를 가진 새 인스턴스를 생성해줬다.

서브 쿼리 예시

@Test
public void subQuery() {
//given
QMember qMember = new QMember("m");
//when
Member findMember = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(qMember.age.max())
.from(qMember)
))
.fetchOne();
//then
assertEquals(40, findMember.getAge());
}

age의 max값을 조회하여 해당 나이를 가진 member를 조회하는 쿼리를 생성한다.

In절

@Test
public void subQueryIn() {
//given
QMember qMember = new QMember("m");
//when
List<Member> findMembers = queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(qMember.age)
.from(qMember)
.where(qMember.age.in(10))
))
.fetch();
//then
assertEquals(1, findMembers.size());
assertEquals(10, findMembers.get(0).getAge());
}

In절에도 서브쿼리를 활용할 수 있다. 하지만 성능상 별로 좋지 않기 떄문에 가급적이면 사용하지 않는 것이 좋다. (참고)


Case 문

Case문은 조건에 따라서 값을 지정해줄 수 있다. Select, Where, OrderBy 에서 사용이 가능하다.

좋은 구조라면 어플리케이션에서 비지니스 로직을 처리해야하기 때문에 쿼리에서 Case 문을 사용하는 것은 안티패턴으로 여겨지기도 한다. 하지만 그 방법이 어려울 경우 사용하면 좋을 것이다.

Case 문 예시

@Test
public void basicCase() {
//when
List<String> results = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
//then
results.forEach(System.out::println);
}

CaseBuilder

@Test
public void complexCase() {
//when
List<String> results = 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();
//then
results.forEach(System.out::println);
}

CaseBuilder를 사용하면 더 편리하게 생성할 수 있다. (여러 엔티티에 대한 조건 생성하기 편함)

NumberExpression으로 정렬

@Test
public void orderByCase() {
//given
NumberExpression<Integer> rankCase = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21,30)).then(1)
.otherwise(3);
//when
List<Tuple> results = queryFactory
.select(member.username, member.age, rankCase)
.from(member)
.orderBy(rankCase.asc())
.fetch();
//then
results.forEach(System.out::println);
}
[member3, 30, 1]
[member1, 10, 2]
[member2, 20, 2]
[member4, 40, 3]

NumberExpression에 CaseBuilder()를 넣어서, 위와 같이 사용할 수 있다. Case에 따라 결정되는 값으로 정렬까지 가능하다.

실행되는 쿼리

select
member0_.username as col_0_0_,
member0_.age as col_1_0_,
case
when member0_.age between ? and ? then ?
when member0_.age between ? and ? then ?
else 3
end as col_2_0_
from
member member0_
order by
case
when member0_.age between ? and ? then ?
when member0_.age between ? and ? then ?
else 3
end asc

쿼리가 위와 같이 실행된다. 신기하다.


상수

Expressions.constant()를 사용해 상수를 사용할 수 있다.

@Test
public void addConstant() {
//given
//when
List<Tuple> results = queryFactory
.select(member.username, constant("A"))
.from(member)
.fetch();
//then
results.forEach(System.out::println);
}

문자 더하기

문자를 더하기 위해선 concat을 사용할 수 있다.

@Test
public void concat() {
//given
//when
String result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.where(member.username.eq("member1"))
.fetchOne();
//then
System.out.println(result);
}

https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84

김영한님의 강의 내용을보고 공부하며 정리한 글입니다.