Search
Duplicate

[Querydsl] 기본문법

상태
Querydsl
담당자
만든날짜
2022/05/11 11:18
수정날짜
2023/12/10 11:49
날짜
목차

기본 문법

시작 - JPQL vs Querydsl
테스트 기본 코드
Querydsl vs JPQL
EntityManger로 JPAQueryFactory 생성
Querydsl은 JPQL 빌더
JPQL vs Querydsl 비교
JPQL: 문자(실행 시점 오류), Querydsl: 코드(컴파일 시점 오류)
JPQL: 파라미터 바인딩 직접, Querydsl: 파라미터 바인딩 자동 처리
JPAQueryFactory를 필드로
Code
JPAQueryFactory를 필드로 제공하면 동시성 문제는 어떻게 될까? 동시성 문제는 JPAQueryFactory를 생성할 때 제공하는 EntitiyManager(em)에 달려있다. 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManger에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다.

기본 Q-Type 활용

Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); // 별칭 직접 지정 Qmember qMember = Qmember.member; // 기본 인스턴스 사용
Java
복사
기본 인스턴스를 static import와 함께 사용
@Test public void startQuerydsl3() { // member1을 찾아라. Member findMember = queryFactory .select(member) .from(member) .where(member.username.eq("member1")) .fetchOne(); assertThat(findMember.getUsername()).isEqualTo("member1"); }
Java
복사
다음 설정을 추가하면 실행되는 JPQL을 볼 수 있다.
spring.jpa.properties.hibernate.use_sql_comments: true
Java
복사
참고: 같은 테이블을 조인해야 하는 경우가아니면 기본 인스턴스를 사용하자

검색 조건 쿼리

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%’ 검색 ...
Java
복사
AND 조건을 파리미터로 처리
@Test public void searchAndParam() { List<Member> result1 = queryFactory .selectFrom(member) .where(member.username.eq("member1"), member.age.eq(10)) .fetch(); assertThat(result1.size()).isEqualTo(1); }
Java
복사
where()에 파라미터로 검색조건을 추가하면 AND 조건이 추가됨
이 경우 null 값은 무시 → 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있음 → 뒤에서 설명

결과조회

fetch(): 리스트 조회, 데이터 없으면 빈 리스트 반환
fetchOne(): 단 건 조회
결과가 없으면: null
결과가 둘 이상이면: com.querydsl.core.NonUniqueResultException
fetchFirst(): limit(1).fetchOne()
fetchResults(): 페이징 정보 포함, total count 쿼리 추가 실행 → 현재는 사용 권장 하지 않음 fetch를 사용을 권장 이후 pageable을 사용한다.
fetchCount(): cont쿼리로 변경해서 count 수 조회
//List List<Member> fetch = queryFactory .selectFrom(member) .fetch(); //단 건 Member findMember1 = queryFactory .selectFrom(member) .fetchOne(); //처음 한 건 조회 Member findMember2 = queryFactory .selectFrom(member) .fetchFirst(); //페이징에서 사용 QueryResults<Member> results = queryFactory .selectFrom(member) .fetchResults(); //count 쿼리로 변경 long count = queryFactory .selectFrom(member) .fetchCount();
Java
복사

정렬

/** * 회원 정렬 순서 * 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(); }
Java
복사
desc(), asc(): 일반 정렬
nullsLast(), nullsFirst(): null 데이터 순서 부여

페이징

조회 건수 제한
@Test public void paging1() { List<Member> result = queryFactory .selectFrom(member) .orderBy(member.username.desc()) .offset(1) //0부터 시작(zero index) .limit(2) //최대 2건 조회 .fetch(); assertThat(result.size()).isEqualTo(2); }
Java
복사
전체 조회 수가 필요하면?
@Test public void paging2() { List<Member> fetch = queryFactory .selectFrom(member) .orderBy(member.username.desc()) .offset(1) .limit(2) .fetch(); Pageable pageRequest = PageRequest.of(1,2, Sort.Direction.DESC,"userName"); PageImpl<Member> page = new PageImpl<>(fetch, pageRequest, fetch.size()); List<Member> memberList = page.getContent(); assertThat(memberList.size()).isEqualTo(2); assertThat(page.getTotalElements()).isEqualTo(4); assertThat(page.getNumber()).isEqualTo(1); assertThat(page.getTotalPages()).isEqualTo(2); }
Java
복사

집합

집합함수
/** * JPQL * select * COUNT(m), //회원수 * SUM(m.age), //나이 합 * AVG(m.age), //평균 나이 * MAX(m.age), //최대 나이 * MIN(m.age) //최소 나이 * from Member m */ @Test public void aggregation() throws Exception { //given List<Tuple> result = queryFactory .select(member.count(), member.age.sum(), member.age.avg(), member.age.max(), member.age.min()) .from(member) .fetch(); //when Tuple tuple = result.get(0); //then 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); }
Java
복사
JPQL이 제공하는 모든 집합 함수를 제공한다.
tuple은 프로젝션과 결과반환에서 설명한다.
GroupBy 사용
/** * 팀의 이름과 각 팀의 평균 연령을 구하라. */ @Test public void group() throws Exception { 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); }
Java
복사
groupBy, 그룹화된 결과를 제한하려면 having
groupBy(), having() 예시
.groupBy(item.price) .having(item.price.gt(1000))
Java
복사

조인 - 기본 조인

기본 조인
조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.
join(조인 대상, 별칭으로 사용할 Q타입)
Java
복사
기본 조인
/** * 팀 A에 소속된 모든 회원 */ @Test public void join() throws Exception { QMember member = QMember.member; QTeam team = QTeam.team; List<Member> result = queryFactory .selectFrom(member) .join(member.team, team) .where(team.name.eq("teamA")) .fetch(); assertThat(result) .extracting("username") .containsExactly("member1", "member2"); }
Java
복사
join(), innerJoin(): 내부 조인(inner join)
leftJoin(): left 외부 조인(left outer join)
rightJoin(): right 외부 조인(right outer join)
JPQL의 on과 서능 최적화를 위한 fetch 조인 제공 → 다음 on 절에서 설명
세타 조인
연관관계가 없는 필드로 조인
/** * 세타 조인(연관관계가 없는 필드로 조인) * 회원의 이름이 팀 이름과 같은 회원 조회 */ @Test public void theta_join() throws Exception { em.persist(new Member("teamA")); em.persist(new Member("teamB")); List<Member> result = queryFactory .select(member) .from(member, team) .where(member.username.eq(team.name)) .fetch(); assertThat(result) .extracting("username") .containsExactly("teamA", "teamB"); }
Java
복사
from 절에 여러 엔티티를 선택해서 세타 조인
외부 조인 불가능 → 다음에 설명할 조인 on을 사용하면 외부 조인 가능

조인 - on절

ON절을 활용한 조인(JPA 2.1부터 지원)
1.
조인 대상 필터링
2.
연관관계 없는 엔티티 외부 조인
1. 조인 대상 필터링
예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인,회원은 모두 조회
/** * 예) 회원과 팀을 조인하면서, 팀 이름이 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 public void join_on_filtering() throws Exception { 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); } }
Java
복사
참고: on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일하다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익순한 where
2.
연관관계 없는 엔티티 외부 조인
예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
/** * 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> fetch = queryFactory .select(member, team) .from(member) .leftJoin(team).on(member.username.eq(team.name)) .fetch(); for (Tuple tuple : fetch) { System.out.println("t = " + tuple); } }
Java
복사
하이버네이트 5.1 부터 on 을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었다. 물론 내부 조인도 가능하다.
주의!문법을 자봐야 한다. leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어간다.
일반조인: leftJon(member.team, team)
on조인: from(member).leftJoin(team).on(xxx)

조인 - 페치 조인

페치 조인은 SQL에서 제공하는 기능은 아니다. SQL조인을 활요해서 연관된 엔티티를 SQL 한번에 조회하는 기능이다. 주로 성능 최적화에 사용하는 방법이다.
페치 조인 미적용
지연로딩으로 Member, Team SQL 쿼리 각각 실행
@Test public void fetchJoinNo() throws Exception { 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(); }
Java
복사
페치 조인 적용
즉시로딩으로Member, Team SQL 쿼리 조인으로 한번에 조회
@Test public void fetchJoinuse() throws Exception { 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(); }
Java
복사
사용방법
join(), leftJoin() 등 조인 기능 뒤에 fetchJoin() 이라고 추가하면 된다.

서브 쿼리

com.querydsl.jpa.JPAExpressions 사용
서브 쿼리 eq 사용
Code
서브 쿼리 goe 사용
Code
서브쿼리 여러건 처리 in 사용
Code
select 절에 subquery
Code
from 절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다. 당연히 Querydsl도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
from 절의 서브쿼리 해결방안
1.
서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2.
애플리케이션에 쿼리를 2번 분리해서 실행한다.
3.
natviceSQL을 사용한다.

Case 문

select, 조건절(where), order by에서 사용 가능
단순한 조건
Code
복잡한 조건
Code
orderBy에서 case문 함께 사용하기
Code
Querydsl은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select 절, orderBy 절에서 함께 사용할 수 있다.

상수, 문자 더하기

상수가 필요하면 Expressions.constant(xxx) 사용
@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); } }
Java
복사
참고: 위와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않는다. 상수를 더하는 것 처럼 최적화가 어려우면 SQL에 constant 값을 넘긴다.
문자 더하기 concat
@Test public void concat() { List<String> fetch = queryFactory .select(member.username.concat("_").concat(member.age.stringValue())) .from(member) .fetch(); for (String concatValue : fetch) { System.out.println("concatValue = " + concatValue); } }
Java
복사
참고: member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있다. 이방법은 ENUM을 처리할 때도 자주 사용한다.