QueryDSL 설정법, 활용법(검색조건쿼리, 기본 문법들)

Gyeongjae Ham·2023년 6월 17일
0

QueryDSL

목록 보기
1/5
post-thumbnail

해당 시리즈는 김영한님의 JPA 로드맵을 따라 학습하면서 내용을 정리하는 글입니다

QueryDSL 설정방법(Spring Boot 3.0 이상)

  • build.gradle의 dependencies에 아래 내용을 추가해주면 됩니다
  • 이 설정으로 하면 build and run 부분을 Intellij로 설정하면 Qclass들을 찾지 못하는 이슈가 있긴 합니다. 추후 이 부분까지 커버하는 설정법을 발견하면 업데이트 하도록 하겠습니다
// Querydsl 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

QueryDSL 라이브러리

  • com.querydsl:querydsl-apt: Qclass를 생성하는 라이브러리입니다
  • com.querydsl:querydsl-jpa: QueryDSL 문법을 사용하게 해주는 라이브러리입니다
  • implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0': 쿼리 파라미터 값을 보게 해주는 라이브러리

JPQL과 QueryDSL

    @Test
    void startJPQL() {
        // memer1을 찾아라
        Member findMember = em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
                .setParameter("username", "member1")
                .getSingleResult();

        assertThat(findMember.getUsername()).isEqualTo("member1");
    }

    @Test
    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은 따지고 보면 그냥 문자열이기 때문에 실행하기 전까지 쿼리를 검증할 수 없는 단점이 있습니다
  • 하지만 QueryDSL은 코드를 작성하는 단계에서 컴파일러가 잘못된 부분을 알려주기 때문에 사용하는데 더 편리하다는 장점을 가지고 있습니다

Q-Type 활용방법

  • Q 클래스 인스턴스를 사용하는 방법은 2가지가 있습니다
  1. 별칭을 직접 지정해서 사용하는 방법
QMember qMember = new QMember("m"); // 별칭 직접 지정
  1. 기본 인스턴스 사용하는 방법
QMember qMember = QMember.member; // 기본 인스턴스 사용
  1. 기본 인스턴스를 사용하는 방법이지만 더 깔끔하게 사용하는 방법(권장하는 방법입니다)
  • static import를 활용한 방법
  • Q클래스에 구현된 변수명으로 사용하는 방법입니다
import static study.querydsl.entity.QMember.*;

@Test
public void startQuerydsl3() {
	// member1을 찾아라
    Member findMember = queryFactory
    						.select(member)
                            .from(member)
                            .where(member.username.eq("member1"))
                            .fetchOne();
	assertThat(findMember.getUsername()).isEqualTo("member1");
  • 같은 테이블명이 존재해서 별칭을 직접 지정해서 구분지어야 하는 경우가 아니라면 그냥 static import 방법을 사용하도록 합니다

검색 조건 쿼리

  • JPQL이 제공하는 모든 검색 조건
member.username.eq("member1") // username = 'member1'
member.username.ne("member1") // username != 'member1'
member.username.eq("member1").not() // username != 'member1'

member.useranme.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%' 검색
  • AND 조건을 파라미터로 처리하기
    • AND 조건을 체인으로 구현하는 방법
@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 조건을 파라미터로 처리하기
    (중간값이 null이 있어도 무시합니다, 추후 동적쿼리 작성에 유리하므로 권장합니다)
@Test
void searchParam() {
	Member findMember = queryFactory
				.selectFrom(member)
				.where(
                        member.username.eq("member1"),
                        member.age.eq(10))
                .fetchOne();

	assertThat(findMember.getUsername()).isEqualTo("member1");
}

결과 조회

  • fetch(): 리스트 조회, 데이터 없으면 빈 리스트를 반환합니다
  • fetchOne(): 단 건 조회
    • 결과가 없으면: null
    • 결과가 둘 이상이면: com.querydsl.core.NonUniqueResultException
  • fetchFirst(): limit(1).fetchOne()
  • fetchResults(): 페이징 정보를 포함, total count 쿼리를 추가 실행
  • fetchCount: 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();

정렬

/**
*회원 정렬 순서
* 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();
}
  • 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);

전체 조회 수가 필요

@Test
public void paging2() {
	QueryResults<Member> queryResults = queryFactory
											.selectFrom(member)
											.orderBy(member.username.desc())
											.offset(1)
											.limit(2)
											.fetchResults();
                                            
	assertThat(queryResults.getTotal()).isEqualTo(4);
	assertThat(queryResults.getLimit()).isEqualTo(2);
	assertThat(queryResults.getOffset()).isEqualTo(1);
	assertThat(queryResults.getResults().size()).isEqualTo(2);
}
  • 주의: count 쿼리가 실행되니 성능상 주의!

    실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있습니다. 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인해 버리기 때문에 성능이 안나올 수도 있습니다. count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 합니다.

집합

집합 함수

/**
* 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 {
	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);
  • JPQL이 제공하는 모든 집합 함수를 제공합니다

GroupBy

/**
 * 팀의 이름과 각 팀의 평균 연령을 구해라.
 */
@Test
public void group() throws Exception {
	List<Tuple> result = queryFactory
    						.select(team.name, member.age.ave())
                            .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);

groupBy(), having()

  • groupBy 그룹화된 결과를 제한하려면 having을 사용하면 됩니다
...
.groupBy(item.price)
.having(item.price.gt(1000))
...

조인

기본 조인

  • 기본 문법
join(조인 대상, 별칭으로 사용할 Q타입)
/**
*팀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");
}
  • join(), innerJoin(): 내부 조인
  • leftJoin(): left 외부 조인\
  • rightJoin(): right 외부 조인

세타 조인

  • 연관관계가 없는 필드로 조인
/**
* 세타 조인(연관관계가 없는 필드로 조인) 
* 회원의 이름이 팀 이름과 같은 회원 조회 
*/

@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");
}
  • from 절에 여러 엔티티를 선택해서 세타 조인
  • 외부 조인 불가능 -> 조인 on을 사용하면 외부 조인이 가능합니다

조인 on절

  • ON절을 활용한 조인
    1. 조인 대상 필터링
    2. 연관관계 없는 엔티티 외부 조인
  1. 조인 대상 필터링
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 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);
    } 
}

ON절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인을 사용하면, WHERE 절에서 필터링 하는 것과 기능이 동일합니다. 따라서 ON절을 활용한 조인 대상 필터링을 사용할 때, 내부조인이면 WHERE 절로 해결하고, 외부조인이 필요한 경우에만 이 기능을 사용하도록 합니다

  1. 연관관계 없는 엔티티 외부조인
  • ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
/**
*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> result = queryFactory
							.select(member, team)
							.from(member)
							.leftJoin(team).on(member.username.eq(team.name))
							.fetch();
                            
	for (Tuple tuple : result) {
		System.out.println("t=" + tuple);
    } 
}
  • leftJoin() 부분을 보면 일반조인할 때와 다르게 파라미터로 엔티티 하나만 들어갑니다
    • 일반조인: leftJoin(member.team, team)
    • ON조인: from(member).leftJoin(team).on(xxx)

페치 조인

  • 페치조인은 SQL에서 제공하는 기능은 아닙니다.
  • SQL 조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능입니다.
  • 주로 성능 최적화에 사용하는 방법입니다

페치 조인 미적용

  • 지연로딩으로 Member, Team 쿼리를 각각 실행합니다
@PersistenceUnit
EntityManagerFactory emf;

@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();

페치 조인 적용

@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(); }
  • 사용법은 join, leftJoin 등 조인 기능 뒤에 fetchJoin이라고 추가하면 됩니다

서브쿼리

  • com.querydsl.jpa.JPAExpressions 사용

서브쿼리 eq 사용

/**
* 나이가 가장 많은 회원 조회
*/

@Test
public void subQuery() throws Exception {
	QMember memberSub = new QMember("memberSub");
	List<Member> result = queryFactory
              .selectFrom(member)
              .where(member.age.eq(
                      JPAExpressions
                              .select(memberSub.age.max())
                              .from(memberSub)
					)).fetch();
                    
	assertThat(result).extracting("age").containsExactly(40);
}

서브쿼리 goe 사용

/**
* 나이가 평균 나이 이상인 회원
*/

@Test
public void subQuery() throws Exception {
	QMember memberSub = new QMember("memberSub");
	List<Member> result = queryFactory
              .selectFrom(member)
              .where(member.age.goe(
                      JPAExpressions
                              .select(memberSub.age.avg())
                              .from(memberSub)
					)).fetch();
                    
	assertThat(result).extracting("age").containsExactly(30, 40);
}

서브쿼리 여러 건 처리 in 사용

/**
* 서브쿼리 여러 건 처리, in 사용
*/

@Test
public void subQuery() throws Exception {
	QMember memberSub = new QMember("memberSub");
	List<Member> result = queryFactory
              .selectFrom(member)
              .where(member.age.in(
                      JPAExpressions
                              .select(memberSub.age)
                              .from(memberSub)
                              .where(memberSub.age.gt(10))
					)).fetch();
                    
	assertThat(result).extracting("age").containsExactly(20, 30, 40);
}

select 절에 subquery

@Test
public void subQuery() throws Exception {
	QMember memberSub = new QMember("memberSub");
	List<Member> result = queryFactory
              					.select(member.username,
                                		JPAExpressions
                                        	.select(memberSub.age.avg())
                                            .from(memberSub)
                                 ).from(member)
}

static import 활용

import static com.querydsl.jpa.JPAExpressions.select;;

List<Member> result = queryFactory
						.selectFrom(member)
                        .where(member.age.eq(
                        	select(memberSub.age.max())
                            from(memberSub)
                         )).fetch();

Case문

  • select, 조건절(where), order by에서 사용 가능합니다

단순한 조건

List<String> result = queryFactory
						.select(member.age
                        		.when(10).then("열살")
                                .when(20).then("스무살")
                                .otherwise("기타"))
                        .from(member)
                        .fetch();

복잡한 조건

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();

orderBy에서 Case문 함께 사용하기

  • ex) 임의의 순서로 회원을 출력하고 싶은 상황
    1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
    2. 0 ~ 20살 회원 출력
    3. 21 ~ 30살 회원 출력
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); }
  • QueryDSL은 자바 코드로 작성하기 때문에 rankPath처럼 복잡한 조건을 변수로 선언해서 select절과 orderBy절에서 함께 사용할 수 있습니다

상수, 문자 더하기

  • 상수가 필요하면 Expressions.constant(xxx) 사용
Tuple result = queryFactory
				.select(member.username, Expressions.constant("A"))
                .from(member)
                .fetchFirst();
  • 문자 더하기 concat
String result = queryFactory
				.select(member.username.concat("_").concat(member.age.stringValue()))
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();

참고: member.age.stringValue() 부분이 중요한데, 문자가 아닌 다른 타입들은 stringValue() 로 문자로 변환할 수 있습니다. 이 방법은 ENUM을 처리할 때도 자주 사용하는 방법입니다!!

profile
Always be happy 😀

0개의 댓글