시작 - JPQL vs Querydsl
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// JPA쿼리 팩토리를 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void startJPQL() { // member1을 찾아라
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
void startQuerydsl() { // member1을 찾아라
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
// JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = QMember.member;
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.username.eq("member1")) // 파라미터 바인딩을 안해도 eq() 를 통해서 자동으로 파라미터 바인딩이 된다.
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
Querydsl은 직접 파라미터를 바인딩 하지 않아도 sql 인잭션 공격을 막기 위해 기본적으로 prepared statement의 파라미터 바인딩 방식을 사용한다.
JPAQueryFactory를 필드 레벨로 제공해도 동시성 문제가 없다. 동시성 문제는 JPAQueryFactory를 생성할 때 제공되는 EntityManager(em)에 달려있다. 그러나 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에 동시성 문제는 걱정하지 않아도 된다. 때문에 springconfig 같은 클래스를 만들어서 JPAQueryFactory를 @autowired로 쓰면 엄청 편하다.
기본 Q-Type 활용
Querydsl 이란 Querydsl 을 통하여 생성되는 정적 Q-type 클래스를 이용하여 SQL 과 같은 쿼리를 생성하도록 도와주는 프레임워크이다. JPA 뿐만 아니라 MongoDB, JDO, Lucene 과 같은 라이브러리도 제공하고 있다.
앞에서 말했던 JPQL의 단점을 완벽하게 커버할 수 있는 Querydsl 은 타입에 안전한 방식으로 쿼리를 실행할 수 있다. 타입을 통하여 쿼리를 작성하므로 도메인 모델의 프로퍼티 변경에 유연하게 대처가 가능하며 강력한 코드 자동완성 기능의 이점을 얻을 수 있고 무엇보다 쿼리를 빠르고 안전하게 만들 수 있다.
Querydsl은 JPQL의 빌더 역할이다.
1. Q 클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; //기본 인스턴스 사용
기본인스턴스를 사용할 때 static import를 사용할 것.
기본 인스턴스를 기본으로 사용하고 같은 테이블을 조인해서 별칭으로 서로 구별을 해줘야할 때만 new 연산자로 사용할 것.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
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
void startQuerydsl() {
Member findMember = queryFactory
.select(member) // QMember.member를 static import해서 사용했다.
.from(member)
.where(member.username.eq("member1")) // 파라미터 바인딩을 안해도 eq() 를 통해서 자동으로 파라미터 바인딩이 된다.
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
}
📌 Application.yml
spring.jpa.properties.hibernate.use_sql_comments: true
참고로 다음 설정을 추가하면 실행되는 JPQL을 볼 수 있다.
검색 조건 쿼리
1. 기본 검색 쿼리
📌 QuerydslBasicTest
@Test
void search() {
Member findMember = queryFactory
.selectFrom(member) // select + from
.where(member.username.eq("member1")
.and(member.age.eq(10)))
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
검색 조건은 .and() / .or()를 메서드 체인으로 연결할 수 있다.
select() / from()을 selectFrom()으로 합칠 수 있다.
2. 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.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%’ 검색
3. AND 조건을 파라미터로 처리
📌 QuerydslBasicTest
@Test
void searchAndParam() {
Member findMember = queryFactory
.selectFrom(member) // select + from
.where(
member.username.eq("member1"),
member.age.eq(10) // 파라미터로 AND() 대체 가능 (이 방식을 더 선호)
)
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
where()에 파라미터로 검색 조건을 추가하면 AND 조건으로 인식한다.
where()의 파라미터에 null 값이 있을 경우 null 값을 무시한다. → 이것을 통해 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있다.
AND() 조건만 있다면 이 방식으로 할 것
결과 조회
fetch() : 리스트 조회, 데이터가 없으면 빈 리스트로 반환한다.
fetchOne() : 단 건 조회.
- 결과가 없으면 : null
- 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
fetchFirst() : limit(1).fetchOne();
fetch().size(); count 쿼리로 변경해서 count 수 조회
📌 QuerydslBasicTest
@Test
void resultFetch() {
// list
List<Member> fetch = queryFactory
.selectFrom(member)
.fetch();
// 단 건
Member member1 = queryFactory
.selectFrom(QMember.member)
.fetchOne();
// 처음 한 건 조회
Member fetchFirst = queryFactory
.selectFrom(QMember.member)
.fetchFirst();
}
🧷 참고
fetchResults() : 페이징 정보 포함. 현재 deprecated 상태.
fetchCount() : count쿼리로 변경해서 count 수 조회. 현재 deprecated 상태
위 두 기능은 현재 deprecated 된 상태다. 왜냐하면 단순한 쿼리에서는 잘 동작하지만 복잡한 쿼리에서는 잘 동작하지 않기 때문이다. 따라서 fetchResults()는 fetch()로 대체한다. 그리고 fetchCount()는 별도로 count 쿼리를 작성해야 한다.
✔ 직접 count 쿼리 작성하기
📌 MemberCount
@Test
void count1() {
Long totalCount = queryFactory
.select(member.count()) // count(member.id)
.from(member)
.fetchOne();
}
@Test
void count2() {
Long totalCount = queryFactory
.select(Wildcard.count) // select count(*)
.from(member)
.fetchOne();
}
count(*)을 사용하고 싶으면 Wildcard.count를 사용하면 된다.
count(member.id)를 사용하고 싶으면 member.count()를 사용하면 된다.
응답 결과는 숫자 하나이므로 fetchOne()을 사용한다.
정렬
📌 QuerydslBasicTest
/**
* 회원 정렬 순서
* 1. 회원 나이 내림차순 (desc)
* 2. 회원 이름 올림차순 (asc)
* 단 2에서 회원 이름이 없으면 마지막에 출력 (nulls last)
*/
@Test
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();
}
/* select
member1
from
Member member1
where
member1.age = ?1
order by
member1.age desc,
member1.username asc nulls last */ 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_
where
member0_.age=?
order by
member0_.age desc,
member0_.username asc nulls last
Comparator 함수형 인터페이스의 static 메서드인 nullsFirst() / nullsLast() 를 이용하여 정렬시 발생하는 NullPointerException 예외를 처리할 수 있다.
nullsFirst() : null 데이터는 맨 앞에 정렬
nullsLast() : null 데이터는 맨 뒤에 정렬
orderBy() : 정렬할 항목들을 파라미터로 전달하면 된다.
desc() : 내림차순
asc() : 오름차순
페이징
📌 QuerydslBasicTest
@Test
void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) // 데이터를 가져오기 시작할 row를 지정. (페이지 번호-1) * 페이지당 데이터 건수
.limit(2) // 페이지당 데이터 건 수
.fetch();
assertThat(result.size()).isEqualTo(2);
}
집합
1. 집합 함수
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
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);
}
}
Querydsl은 JPQL이 제공하는 모든 집합 함수를 제공한다.
Tuple은 querydsl이 제공하는 기능이다.
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있지만, 프로잭션 대상이 둘 이상이면 Tuple이나 DTO로 조회해야 한다. 이처럼 Tuple은 여러 개의 타입을 받을 때 사용한다.
실무에서 Tuple이 자주 쓰이지는 않는다. DTO로 뽑아오는 방식을 더 많이 선호한다.
2. GroupBy 사용
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void group() {
List<Tuple> result = queryFactory
.select(
team.name,
member.age.avg()
)
.from(member)
.join(member.team, team) // member에 있는 team과 team을 조인한다.
.groupBy(team.name) // team의 이름으로 그룹핑을 한다.
.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); // (10+20) / 2
assertThat(teamB.get(team.name)).isEqualTo("teamB");
assertThat(teamB.get(member.age.avg())).isEqualTo(35); // (30+40) / 2
}
}
groupBy() 메서드를 통해 그룹핑을 할 수 있다.
그룹화된 결과를 제한하려면 having()을 사용하면 된다.
🔍 groupBy(), having() 예시
…
.groupBy(item.price) // 아이템 가격으로 그룹핑
.having(item.price.gt(1000)) // 아이템 가격이 1000 초과일 때
...
조인
1. 기본 조인
join(조인 대상, 별칭으로 사용할 Q타입)
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q타입을 지정하면 된다.
🔍 조인 문법
join() / innerJoin() : 내부 조인 (inner join)
leftJoin() : left 외부 조인 (left outer join)
rightJoin() : right 외부 조인 (right outer join)
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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);
}
/**
* 팀 A에 소속된 모든 회원
*/
@Test
void join() {
List<Member> result = queryFactory
.selectFrom(member)
.join(member.team, team)
.where(team.name.eq("teamA"))
.fetch();
assertThat(result)
.extracting("username") //
.containsExactly("member1", "member2");
}
}
extracting(String propertyOrField) : 검증하고자 하는 값이 특정 객체에 속해있을 때 해당 프로퍼티의 이름(변수명)을 파라티머로 넣으면 된다. 참고로 파라미터에 람다도 받을 수 있고, 여러 가지로 오버로딩이 되어있다.
containsExactly(Object... value) : 파라미터로 주어진 값들을 순서대로 포함하고 있는지 확인한다.
extracting 메서드 참고 : https://velog.io/@byeongju/AssertJ%EC%9D%98-extracting
AssertJ 공식 문서 : https://assertj.github.io/doc/
🔍 세타 조인
세타 조인은 연관관계가 없는 필드로 조인하는 것이다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void theta_join() {
em.persist(new Member("teamA")); // 사람 이름이 teamA
em.persist(new Member("teamB")); // 사람 이름이 teamB
em.persist(new Member("teamC")); // 사람 이름이 teamC
List<Member> result = queryFactory
.select(member)
.from(member, team) // 모든 멤버와 팀을 다 가져와서 조인하는 것 (디비가 자체적으로 성능 최적화한다.)
.where(member.username.eq(team.name))
.orderBy(member.username.asc()) // username을 오름차순으로 정렬
.fetch();
assertThat(result)
.extracting("username")
.containsExactly("teamA", "teamB");
}
}
from절에 여러 엔티티를 선택해서 세타 조인을 했다.
세타 조인은 외부 조인이 불가능하다. 그러나 조인 on을 사용하면 외부 조인을 할 수 있다.
2. 조인 - on절
🔍 조인 - on 절 기능
- 조인 대상 필터링을 한다.
- 연관관계 없는 엔티티를 외부 조인한다. (자주 쓰임)
🔍 조인 대상 필터링
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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);
}
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 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
void join_on_filtering() {
List<Tuple> result = queryFactory
.select(member, team) // select 파라미터가 여러개이므로 결과 타입이 Tuple이다.
.from(member)
.leftJoin(member.team, team).on(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
}
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
left join이므로 일단 Member 데이터를 다 가져온다. 그리고 Team 데이터는 Team 이름이 "teamA"인 경우만 데이터를 가져온다.
on절을 활용해 조인 대상을 필터링할 때, 외부조인이 아니라 내부 조인을 사용하면 where 절에서 필터링하는 것과 기능이 동일하다. 따라서 on절을 활용한 조인 대상 필터링을 사용할 때, 내부조인이면 익숙한 where절로 해결하고 정말 외부조인이 필요한 경우에만 on절을 사용할 것.
💡 innerJoin의 where 와 on이 서로 같은지 알아보기
✔ innerJoin-where
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void innerJoin_where() {
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);
}
}
}
/* select
member1,
team
from
Member member1
inner join
member1.team as team
where
team.name = ?1 */ 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
team1_.name=?
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
✔ innerJoin-on
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void innerJoin_on() {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.join(member.team, team).on(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
}
/* select
member1,
team
from
Member member1
inner join
member1.team as team with team.name = ?1 */ 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
and (
team1_.name=?
)
tuple = [Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
tuple = [Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
✔ 결론
내부조인을 사용했을 때 on절과 where절의 출력 결과가 서로 같다는 것을 알 수 있다.
🔍 연관관계가 없는 엔티티 외부 조인
하이버네이트 5.1 버전부터 on()을 이용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가됐다.
물론 내부 조인도 가능하다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void join_on_no_relation() {
em.persist(new Member("teamA")); // 사람 이름이 teamA
em.persist(new Member("teamB")); // 사람 이름이 teamB
em.persist(new Member("teamC")); // 사람 이름이 teamC
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name)) // 연관관계가 없는 조인이므로 on절 조건 맞는 것만 조인이 된다.
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
}
/* select
member1,
team
from
Member member1
left join
Team team with member1.username = team.name */ 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
)
tuple = [Member(id=3, username=member1, age=10), null]
tuple = [Member(id=4, username=member2, age=20), null]
tuple = [Member(id=5, username=member3, age=30), null]
tuple = [Member(id=6, username=member4, age=40), null]
tuple = [Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
tuple = [Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]
tuple = [Member(id=9, username=teamC, age=0), null]
연관관계가 없는 엔티티를 조인할 때는 조심해야할 점이 있다.
leftJoin() 메서드의 파라미터를 보면 일반 조인과 다르게 엔티티가 하나만 존재한다.
- 일반 조인 : leftJoin(member.team, team)
- on 조인 : from(member).leftJoin(team).on(....)
on 조인 처럼 연관관계가 없을시에 leftJoin() 파라미터는 무조건 엔티티가 하나다.
3. 조인 - 페치조인
페치조인은 SQL에서 제공하는 기능은 아니다.
SQL 조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다.
주로 성능 최적화에 사용하는 방법이다.
🔍 페치 조인 미적용
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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);
}
@PersistenceUnit
EntityManagerFactory emf;
@Test
void fetchJoinNo() {
// 영속성 컨텍스트 초기화
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();
System.out.println("findMember = " + findMember.getTeam().getName());
}
}
/* select
member1
from
Member member1
inner join
member1.team as team
where
member1.username = ?1
and member1.team.name = ?2 */ 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
member0_.username=?
and team1_.name=?
// ----------------------------------------
select
team0_.team_id as team_id1_2_0_,
team0_.name as name2_2_0_
from
team team0_
where
team0_.team_id=?
findMember = teamA
지연로딩으로 Member, Team SQL 쿼리를 각각 실행된다. (N+1 문제 발생)
🔍 페치 조인 적용
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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);
}
@PersistenceUnit
EntityManagerFactory emf;
@Test
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();
System.out.println("findMember = " + findMember.getTeam().getName());
}
}
/* select
member1
from
Member member1
inner join
fetch member1.team as team
where
member1.username = ?1 */ 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=?
findMember = teamA
fetchJoin()을 통해 즉시로딩으로 바꿔서 Member, Team를 한 번의 쿼리로 다 가져온다. (N+1 문제 해결)
서브 쿼리
com.querydsl.jpa.JPAExpressions 를 사용하여 서브쿼리를 생성한다. (static import 할 것을 권장)
db는 데이터만 필터링하고 그룹핑하고 조회만 하고 로직은 다 애플리케이션에서 해결해야한다.
ORM을 사용하며, 객체지향적으로 엔티티가 구성되어 있으면 서브쿼리가 필요한 일은 거의 없다.
애초에 서브쿼리는 성능이 안좋기 때문에 안티패턴이다.
1. 서브 쿼리 eq 사용 (where절 서브 쿼리)
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void subQuery() {
QMember memberSub = new QMember("memberSub"); // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)
))
.fetch();
assertThat(result).extracting("age")
.containsExactly(40);
}
}
/* select
member1
from
Member member1
where
member1.age = (
select
max(memberSub.age)
from
Member memberSub
) */ 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_
where
member0_.age=(
select
max(member1_.age)
from
member member1_
)
위의 서브 쿼리를 쿼리 2개로 나눠봤다.
📌 QuerydslBasicTest
/**
* 서브 쿼리를 2개의 쿼리로 분리
*/
@Test
void subQueryChangeTwoQuery() {
Integer maxAge = queryFactory // 최대 나이 구하기
.select(member.age.max())
.from(member)
.fetchOne();
System.out.println("maxAge = " + maxAge);
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(maxAge))
.fetch();
System.out.println("result = " + result);
}
/* select
max(member1.age)
from
Member member1 */ select
max(member0_.age) as col_0_0_
from
member member0_
maxAge = 40
// --------------------------------------
/* select
member1
from
Member member1
where
member1.age = ?1 */ 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_
where
member0_.age=?
result = [Member(id=6, username=member4, age=40)]
2. 서브 쿼리 goe 사용 (where절 서브 쿼리)
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void subQueryGoe() {
QMember memberSub = new QMember("memberSub"); // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
))
.orderBy(member.age.asc())
.fetch();
assertThat(result).extracting("age")
.containsExactly(30, 40);
}
}
/* select
member1
from
Member member1
where
member1.age >= (
select
avg(memberSub.age)
from
Member memberSub
)
order by
member1.age asc */ 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_
where
member0_.age>=(
select
avg(cast(member1_.age as double))
from
member member1_
)
order by
member0_.age asc
서브쿼리를 2개의 쿼리로 분리시켰다.
📌 QuerydslBasicTest
/**
* 서브 쿼리를 2개로 분리
*/
@Test
void subQueryGoeChangeTwoQuery() {
List<Double> avgAge = queryFactory
.select(member.age.avg())
.from(member)
.fetch();
System.out.println("avgAge = " + avgAge);
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.goe(avgAge))
.fetch();
System.out.println("result = " + result);
}
/* select
avg(member1.age)
from
Member member1 */ select
avg(cast(member0_.age as double)) as col_0_0_
from
member member0_
avgAge = 25.0
// ----------------------------------------------
/* select
member1
from
Member member1
where
member1.age >= ?1 */ 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_
where
member0_.age>=?
result = [Member(id=5, username=member3, age=30), Member(id=6, username=member4, age=40)]
3. 서브쿼리 여러 건 처리 in 사용 (where절 서브쿼리)
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void subQueryIn() {
QMember memberSub = new QMember("memberSub"); // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(memberSub.age.gt(10))
))
.orderBy(member.age.asc())
.fetch();
assertThat(result).extracting("age")
.containsExactly(20, 30, 40);
}
}
/* select
member1
from
Member member1
where
member1.age in (
select
memberSub.age
from
Member memberSub
where
memberSub.age > ?1
)
order by
member1.age asc */ 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_
where
member0_.age in (
select
member1_.age
from
member member1_
where
member1_.age>?
)
order by
member0_.age asc
📌 QuerydslBasicTest
@Test
void subQueryInChangeTwoQuery() {
List<Integer> gtAge10 = queryFactory
.select(member.age)
.from(member)
.where(member.age.gt(10))
.fetch();
System.out.println("gtAge10 = " + gtAge10);
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.in(gtAge10))
.fetch();
System.out.println("result = " + result);
}
/* select
member1.age
from
Member member1
where
member1.age > ?1 */ select
member0_.age as col_0_0_
from
member member0_
where
member0_.age>?
gtAge10 = [20, 30, 40]
// ------------------------------------------------
/* select
member1
from
Member member1
where
member1.age in ?1 */ 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_
where
member0_.age in (
? , ? , ?
)
result = [Member(id=4, username=member2, age=20), Member(id=5, username=member3, age=30), Member(id=6, username=member4, age=40)]
4. select절에 서브쿼리
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void selectSubQuery() {
QMember memberSub = new QMember("memberSub"); // 별칭(alias)가 겹치면 안되기 때문에 새로운 인스턴스 생성.
List<Tuple> result = queryFactory
.select(member.username,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)
)
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
}
/* select
member1.username,
(select
avg(memberSub.age)
from
Member memberSub)
from
Member member1 */ select
member0_.username as col_0_0_,
(select
avg(cast(member1_.age as double))
from
member member1_) as col_1_0_
from
member member0_
tuple = [member1, 25.0]
tuple = [member2, 25.0]
tuple = [member3, 25.0]
tuple = [member4, 25.0]
5. from절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다.
🔍 from절의 서브 쿼리 해결방안
- 서브쿼리를 join으로 변경한다. (가능할 때도 있고, 불가능할 때도 있다.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
- nativeSQL을 사용한다.
참고로 db는 데이터만 필터링하고 그룹핑하고 조회만 하고 로직은 다 애플리케이션에서 해결해야한다.
Case 문
case문은 select / 조건절(where) / order by 에서 사용 가능하다.
그러나 case의 계산 조건들은 DB에서 하는 것 보다 애플리케이션에서 로직을 작성하는 것이 좋다.
1. 단순한 조건
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void basicCase() {
List<String> result = queryFactory
.select(member.age
.when(10).then("열살") // 10살일 때
.when(20).then("스무살") // 20살일 때
.otherwise("기타") // 나머지
)
.from(member)
.fetch();
System.out.println("result = " + result);
}
}
/* select
case
when member1.age = ?1 then ?2
when member1.age = ?3 then ?4
else '기타'
end
from
Member member1 */ select
case
when member0_.age=? then ?
when member0_.age=? then ?
else '기타'
end as col_0_0_
from
member member0_
result = [열살, 스무살, 기타, 기타]
2. 복잡한 조건
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
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();
System.out.println("result = " + result);
}
}
/* select
case
when (member1.age between ?1 and ?2) then ?3
when (member1.age between ?4 and ?5) then ?6
else '기타'
end
from
Member member1 */ select
case
when member0_.age between ? and ? then ?
when member0_.age between ? and ? then ?
else '기타'
end as col_0_0_
from
member member0_
result = [0~20살, 0~20살, 21~30살, 기타]
3. 예제
예를들어서 다음과 같이 임의의 순서로 회원을 출력하고 싶다면?
- 1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
- 2. 0 ~ 20살 회원 출력
- 3. 21 ~ 30 살 회원 출력
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void anotherComplexCase() {
NumberExpression<Integer> rankPath = new CaseBuilder()
.when(member.age.between(0, 20)).then(2)
.when(member.age.between(21, 30)).then(1)
.otherwise(3);
System.out.println("rankPath = " + rankPath);
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);
}
}
}
rankPath = case when member1.age between 0 and 20 then 2 when member1.age between 21 and 30 then 1 else 3 end
/* select
member1.username,
member1.age,
case
when (member1.age between ?1 and ?2) then ?3
when (member1.age between ?4 and ?5) then ?6
else 3
end
from
Member member1
order by
case
when (member1.age between ?7 and ?8) then ?9
when (member1.age between ?10 and ?11) then ?12
else 3
end desc */ 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
username = member4 age = 40 rank = 3
username = member1 age = 10 rank = 2
username = member2 age = 20 rank = 2
username = member3 age = 30 rank = 1
상수, 문자 더하기
1. 상수 더하기
상수가 필요하면 Expressions.constant(xxx) 메서드를 사용하면 된다.
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
void constant() {
List<Tuple> result = queryFactory
.select(member.username, Expressions.constant("A"))
.from(member)
.fetch();
System.out.println("result = " + result);
}
}
/* select
member1.username
from
Member member1 */ select
member0_.username as col_0_0_
from
member member0_
result = [[member1, A], [member2, A], [member3, A], [member4, A]]
상수는 JPQL 쿼리에서 안나가고 결과에서만 추가해서 나간다.
2. 문자 더하기 concat
📌 QuerydslBasicTest
package study.querydsl;
@SpringBootTest
@Transactional
public class QuerydslBasicTest {
// 필드 레벨로 가져가도 된다. 동시성 문제를 걱정안해도 된다.
JPAQueryFactory queryFactory;
@Autowired
EntityManager em;
@BeforeEach
public void before() {
// JPA쿼리 팩토리를 만들 때 파라미터로 엔티티 매니저를 넘겨줘야한다.
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
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);
}
}
}
/* select
concat(concat(member1.username,
?1),
str(member1.age))
from
Member member1
where
member1.username = ?2 */ select
((member0_.username||?)||cast(member0_.age as char)) as col_0_0_
from
member member0_
where
member0_.username=?
s = member1_10
member.age.stringValue() 부분이 중요하다. 문제가 아닌 다른 타입들은 stringValue()를 통해 문자로 변환할 수 있다. 이 방법은 ENUM을 문자열로 바꿀 때 자주 사용한다.
👀 참고 자료
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84/dashboard
https://devwithpug.github.io/java/querydsl-with-datajpa/
https://jojoldu.tistory.com/379
'[JPA] > 실전! Querydsl' 카테고리의 다른 글
[Querydsl] 스프링 데이터 JPA가 제공하는 Querydsl 기능 (0) | 2022.04.29 |
---|---|
[Querydsl] 스프링 데이터 JPA 와 Querydsl (0) | 2022.04.28 |
[Querydsl] 실무 활용 - 순수 JPA와 Querydsl (0) | 2022.04.27 |
[QueryDsl] 중급 문법 (0) | 2022.04.26 |
[QueryDSL] 프로젝트 환경 설정 (0) | 2022.04.24 |