Queydsl 기본 문법 (Q-Type, 검색 조건, 결과 조회, 정렬, 페이징, 집합, 조인, 페치 조인, 서브 쿼리, Case문, 상수, 문자 더하기)
예제 도메인 모델
- 엔티티 클래스
- ERD
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public Member(String username) {
this(username, 0);
}
public Member(String username, int age) {
this(username, age, null);
}
public Member(String username, int age, Team team) {
this.username = username;
this.age = age;
if (team != null) {
chageTeam(team);
}
}
public void chageTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
@Id
@GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
JPQL vs Querydsl
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void befor() {
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);
}
@Test
public void startJPQL() {
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");
}
}
JPQL
문자 자체를 실행한다
Runtime Error가 발생한다
실행 해봐야 예외를 알 수 있다
파라미터 바인딩을 직접 명시해야 한다
Querydsl
JPQL 빌더 역할을 한다
코드를 실행한다
Compile Error가 발생한다
실행 전에 예외를 알 수 있다
파라미터 바인딩이 자동 처리된다
JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까?
동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntityManager(em)에 달려있다
스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다
기본 Q-Type 활용
- Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
- 기본 인스턴스를 static import와 함께 사용
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void startQuerydsl() {
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.username.eq("member1")) // 파라미터 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
검색 조건 쿼리
- 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.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%’ 검색
...
- 기본 검색 쿼리
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@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");
}
}
- AND 조건을 파라미터로 처리
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@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");
}
}
where() 에 파라미터로 검색조건을 추가하면 AND 조건이 추가된다
이 경우 null 값은 무시한다
메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있다(뒤에서 설명)
결과 조회
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void resultFetch() {
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
Member fetchOne = queryFactory
.selectFrom(member)
.fetchOne();
Member fetchFirst = queryFactory
.selectFrom(member)
.fetchFirst();
}
}
fetch()
리스트 조회
데이터 없으면 빈 리스트 반환
fetchOne()
단 건 조회
결과가 없으면 null
결과가 둘 이상이면
com.querydsl.core.NonUniqueResultException
발생
fetchFirst()
- limit(1).fetchOne()
정렬
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
/**
* 회원 정렬 순서
* 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()).isNull();
}
}
페이징
import static study.querydsl.entity.QMember.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void paging() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetch();
assertThat(result.size()).isEqualTo(2);
}
@Test
public void count() {
Long totalCount = queryFactory
//.select(Wildcard.count) //select count(*)
.select(member.count()) //select count(member.id)
.from(member)
.fetchOne();
System.out.println("totalCount = " + totalCount);
}
}
집합
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@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);
}
/**
* 팀의 이름과 각 팀의 평균 연령을 구해라
*/
@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);
}
}
- JPQL이 제공하는 모든 집합 함수를 제공한다
조인 - 기본 조인
1. join
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
/**
* 팀 A에 소속된 모든 회원
*/
@Test
public void join() {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
// .leftJoin(member.team, team)
// .rightJoin(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("member1", "member2");
}
}
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
team1_.name=?
엔티티가 두 개가 들어간다
외부키를 기준으로 조인한다
2. 세타 조인
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
/**
* 세타 조인
* 회원의 이름이 팀 이름과 같은 회원 조회
*/
@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");
}
}
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
cross join
team team1_
where
member0_.username=team1_.name
from 절에 여러 엔티티를 선택해서 연관관계가 없는 필드도 세타 조인 할 수 있다
단 Outter 조인이 불가능하다
on절을 사용하면 Outter 조인이 가능하다
조인 - on절
1. 조인 대상 필터링
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
/**
* 예) 회원과 팀 이름이 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);
}
}
}
select
member0_.member_id as member_i1_1_0_,
team1_.team_id as team_id1_2_1_,
member0_.age as age2_1_0_,
member0_.team_id as team_id4_1_0_,
member0_.username as username3_1_0_,
team1_.name as name2_2_1_
from
member member0_
left outer join
team team1_
on member0_.team_id=team1_.team_id
and (
team1_.name=?
)
on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다
내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용하자
2. 연관관계 없는 엔티티 외부 조인
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
/**
* 연관관계가 없는 엔티티 외부 조인
* 회원의 이름이 팀 이름과 같은 대상을 외부 조인
*/
@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);
}
}
}
select
member0_.member_id as member_i1_1_0_,
team1_.team_id as team_id1_2_1_,
member0_.age as age2_1_0_,
member0_.team_id as team_id4_1_0_,
member0_.username as username3_1_0_,
team1_.name as name2_2_1_
from
member member0_
left outer join
team team1_
on (
member0_.username=team1_.name
)
- leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다
조인 - 페치 조인
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@Test
public void fetchJoinUse() {
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();
}
}
select
member0_.member_id as member_i1_1_0_,
team1_.team_id as team_id1_2_1_,
member0_.age as age2_1_0_,
member0_.team_id as team_id4_1_0_,
member0_.username as username3_1_0_,
team1_.name as name2_2_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.username=?
서브 쿼리
import static com.querydsl.jpa.JPAExpressions.*;
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
/**
* 나이가 가장 많은 회원 조회
*/
@Test
public void subQueryMax() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
select(memberSub.age.max())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(40);
}
/**
* 나이가 평균 이상인 회원 조회
*/
@Test
public void subQueryAvg() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
select(memberSub.age.avg())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(30, 40);
}
@Test
public void subQueryIn() {
QMember memberSub = new QMember("memberSub");
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(20, 30, 40);
}
@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);
}
}
}
com.querydsl.jpa.JPAExpressions
을 사용한다- static import할 수 있다
from 절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 select, from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다
당연히 Querydsl 도 지원하지 않는다
하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다
Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다
from 절의 서브쿼리 해결방안
서브쿼리를 join으로 변경한다 (가능한 상황도 있고, 불가능한 상황도 있다)
애플리케이션에서 쿼리를 2번 분리해서 실행한다
nativeSQL을 사용한다
Case 문
import static com.querydsl.jpa.JPAExpressions.*;
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@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);
}
}
/**
* 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
* 1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
* 2. 0 ~ 20살 회원 출력
* 3. 21 ~ 30살 회원 출력
*/
@Test
public void complexCaseWithOrderBy() {
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);
}
}
}
// complexCaseWithOrderBy
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 desc
select, 조건절(where), order by에서 사용 가능하다
Querydsl은 자바 코드로 작성하기 때문에 rankPath 처럼 복잡한 조건을 변수로 선언해서 select 절, orderBy 절에서 함께 사용할 수 있다
상수, 문자 더하기
import static com.querydsl.jpa.JPAExpressions.*;
import static study.querydsl.entity.QMember.*;
import static study.querydsl.entity.QTeam.*;
...
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
...
@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);
}
}
@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을 처리할 때도 자주 사용한다
참고 자료