[Querydsl] 기본 문법

hi·2023년 1월 17일
0

JPAQueryFactory

JPAQueryFactory queryFactory = new JPAQueryFactory(em);
  • Querydsl을 사용하려면 JPAQueryFactory 필요
    👉 JPAQueryFactory는 JPA 쿼리인 JPQL을 만들기 때문에 EntityManager 필요

  • JPAQueryFactory 를 스프링 빈으로 등록하여 사용할 수도 있다

    • 의존관계 주입시 두가지여서 test시 조금 귀찮음
    • @RequiredArgsConstructor 사용 가능
  • 필드로 제공해도 동시성 문제 X
    👉 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문

//빈 등록 후
@Bean
JPAQueryFactory jpaQueryFactory(EntityManager em) {
	return new JPAQueryFactory(em);
}

//주입 받아 사용
public MemberJpaRepository(EntityManager em, JPAQueryFactory queryFactory) {
	this.em = em;
	this.queryFactory = queryFactory;
}
  • 스프링이 주입해주는 엔티티 매니저는 프록시용 가짜 엔티티 매니저.
    실제 사용 시점에 트랜잭션 단위로 실제 엔티티 매니저(영속성 컨텍스트)를 할당

Q-Type

Q클래스 인스턴스 사용 방법 2가지

QMember qMember = new QMember("m"); //별칭 직접 지정
QMember qMember = QMember.member; 	//기본 인스턴스 사용
  • 기본 인스턴스 static import 사용 가능
  • 같은 테이블을 조인하는 경우가 아니라면 기본 인스턴스 사용

ex)

@Test
public void querydsl() {

	Member findMember = queryFactory
			.selectFrom(member)
 			.where(member.username.eq("member1")
 				.and(member.age.eq(10)))
 			.fetchOne();
 
 	assertThat(findMember.getUsername()).isEqualTo("member1");
}

selectFrom

select() + from() 👉 selectFrom() 으로 사용 가능

검색 조건

.and() .or()

  • 메서드 체인으로 연결 가능
  • JPQL이 제공하는 모든 검색 조건 제공
.eq() 
.ne() 

.goe()
.gt()
.loe()
.lt()

.not() 
.in() 
.notIn() 
.between()

.like()		 // like 검색
.contains()	 // like '%member%' 검색
.startWith() // like 'member%' 검색

where( ) 검색 조건 추가

.where(member.age.eq(20), member.gender.eq("F"), ... )
  • where()에 파라미터로 검색조건을 추가하면 AND 조건이 추가된다
  • null 값은 무시

결과 조회

fetch()

  • 리스트 조회, 데이터 없으면 빈 리스트 반환

fetchOne()

  • 단 건 조회
    • 결과가 없으면 null
    • 결과가 둘 이상이면 com.querydsl.core.NonUniqueResultException

fetchFirst()

  • limit(1).fetchOne( )

fetchResults()
- 페이징 정보 포함
- total count 쿼리 추가 실행 👉 총 쿼리 2번
- 조인이 붙으며 쿼리가 복잡해지면, count 쿼리와 데이터 조회 쿼리가 다를 수 있음
(count 쿼리는 조인이 필요 없는 경우) 👉 성능 최적화 필요시, 쿼리 별도로 실행

results.getTotal();   //토탈 카운트
results.getResults(); //데이터

fetchCount()
- count 쿼리로 변경해서 count 수 조회

더 이상 fetchResults() , fetchCount() 를 지원하지 않는다고 함

fetchOne 사용


정렬

desc() asc()

nullsLast() nullsFirst() null 데이터 순서 부여

List<Member> result = queryFactory
		.selectFrom(member)
		.where(member.city.eq("서울"))
		.orderBy(member.username.asc().nullsLast)
		.fetch();

중복

.distinct()

.select(member.username).distinct()

페이징

.offset() (index 0부터 시작)
.limit()

fetch() 조회 건수 제한
fetchResults() 전체 조회 (count 쿼리 성능 주의) , 카운트 쿼리시 필요없는 order by 제거

더 이상 fetchResults() , fetchCount() 를 지원하지 않는다고 함

fetchOne 사용

집합 함수

.count() .sum() .avg() .max() .min()

  • count(*) 대신 Wildcard.count 사용
  • member.count() 를 사용하면 count(member.id) 로 처리
List<Tuple> result = queryFactory 
		.select(
        		member.count(), //-> select count(member.id)
                member.age.sum(),
                member.age.avg(),
                member.age.max(),
                member.age.min()
        )
        .from(member)
        .fetch();
        
Tuple tuple = result.get(0);

💡 조회하는 대상이 여러개이면 Tuple 사용
but, 실무에서는 이것보다 DTO로 변환하는 방법 사용

그룹

.groupBy
.having 그룹화된 결과 제한


조인

기본 조인

join(조인 대상, 별칭으로 사용할 Q타입)

join() innerJoin() leftJoin() rightJoin()

세타 조인

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

  • outer join 불가 👉 on절 사용

on절

.on()

  1. 외부 조인 조건 추가
    내부 조인의 where절 필터링과 같은 기능
  1. 세타 조인 outer join
List<Tuple> result = queryFactory
		.select(member, team)
		.from(member)
 		.leftJoin(team).on(member.username.eq(team.name))
		.fetch();
  • 일반 조인과 다르게 leftJoin() 에 엔티티 하나만 들어감
.leftJoin(member.team, team) 		//일반 조인

from(member).leftJoin(team).on(xxx) // on 조인

페치 조인

.fetchJoin()

  • SQL조인을 활용하여 연관된 엔티티를 SQL 한번에 조회
  • 주로 성능 최적화에 사용
.join(member.team, team).fetchJoin()

서브 쿼리

  • com.querydsl.jpa.JPAExpressions 사용
  • 외부, 내부 쿼리의 별칭이 달라야 함
  • static import 가능
@Test
public void subQuery() throws Exception {
	
    QMember memberSub = new QMember("memberSub");		//다른 별칭

	List<Member> result = queryFactory
 			.selectFrom(member)
 			.where(member.age.eq(
 					
                    JPAExpressions						//static import 가능
 							.select(memberSub.age.max())
 							.from(memberSub)
 			))
 			.fetch();
            
 	assertThat(result).extracting("age").containsExactly(20);
}

in절

.selectFrom(member)
.where(member.age.in(

		select(memberSub.age)
        		.from(memberSub)
                .where(memberSub.age.gt(10))
))
  • select 절에도 사용 가능

💡참고
화면에 쿼리를 그대로 맞추려고 하다 보니 계속 서브쿼리가 생겨나고 복잡해진다.
데이터를 필터링, 그룹핑하여 가져오는 용도로만 쓰고 그 외에는 애플리케이션, 프레젠테이션 로직에서 끝내야 한다.
한방쿼리가 정말 좋은지 고민해보자.
한 번에 가져오기 위해 복잡해지는 쿼리보다, 단순한 여러 개의 쿼리가 더 좋을 수도 있다. 서브쿼리를 줄이기 위해서는 생각의 전환이 필요 (case문 등..)
- 킹영한님


Case

.when().then()
.otherwise()

  • select, 조건절(where), order by에서 사용 가능
  • order by에서 사용시 복잡한 조건을 변수화하여 사용

단순한 조건

.select(member.age
		.when(10).then("A")
 		.when(20).then("B")
 		.otherwise("C"))
.from()

복잡한 조건

CaseBuilder()

.select(new CaseBuilder()
		.when(member.age.between(0, 10)).then("A")
 		.when(member.age.between(11, 20)).then("B")
 		.otherwise("C"))
.from()

order by

NumberExpression<Integer> rankPath = new CaseBuilder()
		.when(member.age.between(0, 20)).then(1)
		.when(member.age.between(21, 30)).then(2)
		.otherwise(3);
 
List<Tuple> result = queryFactory
 	.select(member.username, member.age, rankPath)
 	.from(member)
 	.orderBy(rankPath.desc())
 	.fetch();

상수

Expressions.constant()

.select(member.username, Expressions.constant("A"))
  • 상수를 더하는 것처럼 최적화가 가능하면 SQL에 상수 값을 넘기지 않고 (출력 X)
    최적화가 어려우면 값을 넘긴다

concat

.concat()

  • 문자 외 : stringValue 로 타입 변환
  • enum 타입의 경우 자주 사용
.select(member.city.concat("_").concat(member.age.stringValue()))

벌크 연산

데이터 수정

long count = queryFactory
		.update(member)
    	.set(member.username, "회원")
    	.where(member.group.eq("A"))
    	.execute();
        
long count = queryFactory
		.update(member)
		.set(member.age, member.age.add(1))
		.execute();
        
//곱셈
long count = queryFactory
		.update(member)
		.set(member.age, member.age.multiply(5))
		.execute();

데이터 삭제

long count = queryFactory
		.delete(member)
     	.where(member.group.eq("A"))
		.execute();

🔎 JPQL 과 마찬가지로 영속성 컨텍스트를 무시하고 실행된다.
따라서 이후 영속성 컨테스트를 초기화 하는 것이 안전


SQL function 호출

  • JPA와 같이 Dialect에 등록된 내용만 호출 가능

ex. replace (member -> M)

String result = queryFactory
		.select(Expressions.stringTemplate(
        		"function('replace', {0}, {1}, {2})", member.username, "member", "M"))
		.from(member)
		.fetchFirst();


ex. 소문자로 변경

.select(member.username)
.from(member)
.where(member.username.eq(Expressions.stringTemplate(
							"function('lower', {0})", member.username)))
  • ansi 표준 함수들은 querydsl에 상당수 내장되어 있으므로 아래와 같이 처리 가능
.where(member.username.eq(member.username.lower()))

0개의 댓글