관리 메뉴

JIE0025

[Spring] QueryDSL 본문

백엔드/스프링, 스프링부트, JPA, Spring Webflux

[Spring] QueryDSL

Kangjieun11 2022. 11. 13. 01:24
728x90

 

개인적으로 공부하면서 정리한 내용입니다 : )

 

 


 

 

 

FLAG Project - BE Study 

2022.11.10





FLAG

 

 

 

 

 


 

 

QueryDSL

 

  • 왜 QueryDSL을 사용할까?
    • SQL, JPQL의 문제점
    • JPA Criteria 
    • 문제의 해결 : QueryDSL
  • QueryDSL의 장점  
  • 동적쿼리 vs 정적쿼리
  • JPQL vs QueryDSL
  • Q-type class
  • 기본문법
    • 검색조건 쿼리
    • AND 조건 (파라미터로 처리)
    • 나중에 추가 예정 (결과 조회, 정렬, 페이징, 집합, 기본 조인, 서브쿼리)
  • 구현 예제

 

 


 

✅ QueryDSL?

JPQL의 작성을 도와주는 오픈소스 라이브러리

정적 타입을 이용해서 SQL과 같은 쿼리를 생성 할 수 있게 해주는 프레임워크

쿼리를 자바코드로 작성할 수 있게 도와주는 기술

 

(개 복잡한 쿼리를 만들기 위한 추가 도구 라고 보면 됨)

 

 


 

 

 왜 QueryDSL을 사용할까? (WHY)

 

1- SQL과 JPQL의 문제점

  • SQL, JPQL문자열로 Type-check 불가능 
    • 문자열이라서 동적쿼리 작성하는 것이 어려움
  •  문법 오류를  애플리케이션 로딩 시점에 알 수 있다. ( 컴파일 시점엔 알 수 없다.)
  • 로직 실행 전까지 작동 여부를 알수 없다. 
  • 쿼리 실행 시점에 오류를 발견한다. 

 

2-  그래서 나온 JPA Criteria코드로 JPQL을 작성해서 문법 오류를 컴파일 단계에서 잡을 수 있게됨

또 동적 쿼리를 편리하게 작성할 수 있게 됨.

 

but)     Criteria는 굉장히 복잡하고 어려움

                     →  작성된 코드의 복잡성으로 어떤 JPQL이 생성될지 파악하기 쉽지 않음

 

 

3- 문제의 해결 (QueryDSL)

✔   쿼리를 문자가 아닌 자바코드로 작성해 쉽고 간결하게,

모양도 쿼리와 비슷하게 개발할 수 있도록 하는 것이 QueryDSL

 

 

 

⏺  QueryDSL의 장점 👍

  • 문자가 아닌 자바코드로서 작성
  • 컴파일 시점에 문법 오류 발견
    • 자바 코드이기 때문에 자바 컴파일러가 검증 가능하다
  • IDE의 도움으로 코드 자동완성
  • 단순하고, 쉬움 (코드의 모양이 JPQL과 흡사, 쿼리스럽고 직관적임)
  • 동적 쿼리 작성이 편리 // 자바코드로 작성하니까 겠지?
  • 유지보수에 용이

 

⏺  QueryDSL의 단점 

  • 코딩을 더 많이 해야함 ㅋㅋ

 

* 너무 복잡한 쿼리를 사용할 때만 QueryDSL을 사용하는걸 권장하는 것 같음

* 적당한 쿼리는 JPA 함수 쓰기..(너무 비효율적이고 느리면 QueryDSL로 가는걸 추천한다)

 

 

⏺  동적쿼리 vs 정적쿼리

◼ 정적쿼리 (Static)

어떤 조건/상황에도 변경되지 않는 쿼리

어떻게 DB에 접근할지가 미리 정의되는 형태의SQL, SQL구문이 정적으로 "컴파일시점"에 정의 됨.

→ 변수를 사용하지 않는, 일반적으로 작성된 쿼리

 

 

◼ 동적쿼리 (Dynamic)

특정 조건/상황에 따라 변경되는 쿼리

런타임에서 사용자의 입력값에 따라 동적으로 SQL구문을 생성해 실행하는 방식

→  <값이 바뀌는 변수>를 넣어 쿼리문 작성

 

 

 

⏺  JPQL vs QueryDSL

💻 JPQL

@Test
void startJPQL() {
  Member findByJPQL = em.createQuery(
                  "select m from Member m " +
                          "where m.username = :username", Member.class)
          .setParameter("username", "member1")
          .getSingleResult();

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

username이 일치하는 멤버를 조회함.

 

문제점

1) 문자열 작성으로 컴파일 단계에서 오류를 발견할 수 있음. 

2) 사소한 띄어쓰기로 예외가 발생할 수 있지만, 실제로 코드가 동작할때까진 오류를 발견할 수 없음.

 

💻 QueryDSL

@Test
void startQuerydsl() {
  QMember m = new QMember("m");

  Member findMember = jpaQueryFactory
          .select(m)
          .from(m)
          .where(m.username.eq("member1"))
          .fetchOne();

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

위의 JPQL예제와 동일한 결과를 가져오지만, 쿼리에서 사용하는 명령어를 자바코드로서 작성한다. 

 

 


 

✅  Q-type class

  • QueryDsl을 사용하면, Q-type을 통해 쿼리를 작성한다. 
  • QueryDSL 설정을 성공적으로 마치면, @Entity가 붙은 클래스를 찾아 자동 생성된다. 
    • @Entity가 붙은 User.java 클래스가 있다면 QUser.java 파일이 자동으로 생성되는것
  • Q타입 == 쿼리타입
  • Q-type의 class들은 QueryDSL을 사용하여 메소드 기반으로 쿼리를 작성할 때
    우리가 만든 도메인 클래스의 구조를 설명해주는 메타데이터 역할을 하며 쿼리의 조건을 설정할 때 사용됨.

 

⏺  Q클래스 인스턴스를 사용하는 2가지 방법

1) 별칭 직접 지정

QMember qMember = new QMember("m");

→ JPQL실행시 select m From Member m ~~ 식으로 작성

같은 테이블을 Join / 서브쿼리 사용하는 경우가 아니면 잘 사용 안함.

 

 

2) 기본 생성 인스턴스 사용

QMember qMember = QMember.member;

→ static-import 사용

import static jpabook.jpashop.domain.QMember.member; // 기본 인스턴스

 

 

 


 

 

✅  기본 문법

 

⏺  검색조건 쿼리

 

QueryDSL 기본 쿼리 기능 코드 

JPAQuery query = new JPAQuery(em);
QItem item = QItem.item;
List<Item> list = query.from(item)
	.where(item.name.eq("좋은상품").and(item.price.gt(20000)))
    .list(item);

얘를 실행하면 실행된 JPQL은 

select item
from Item item
where item.name = ?1 and item.price > ?2

//대충 ?1 과 ?2가 동적쿼리- 변수부분임

 

where절에는 and/ or을 사용할 수 있다.

 

@Test
void search() {
  Member findMember = jpaQueryFactory
          .selectFrom(member)
          .where(member.username.eq("member1")
                  .and(member.age.eq(10)))
          .fetchOne();

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

 

 

아래는 시간이 없어서 나중에 여유롭거나 직접 사용할 때쯤 추가할 예정이다.

⏺  AND 조건은 파라미터로 처리

⏺  결과 조회

⏺  정렬

⏺  페이징

⏺  집합

⏺  기본 조인

⏺  서브쿼리

 


 

✅  구현 예제

 

1) entityManager 주입받기

QueryDSL이 쿼리를 생성할 수 있도록 엔티티매니저를 주입해야한다. 

 

💻 QueryDslConfig

@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory(){
        return new JPAQueryFactory(entityManager);
    }
}

 

 

 

2) Spring data JPA를 구현할 repository를 인터페이스로 만들고,

JpaRepository와 사용자 정의 Repository ( MemberRepositoryCustom )를 상속받는다. 

 

💻 MemberRepository

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
	List<Member> findByUsername(String username);
}

 

 

3) QueryDsl을 사용하려면 사용자 정의 Reppository Interface를 만들어줘야한다. 

 

💻 MemberRepositoryCustom

public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(SearchCond searchCond);
}

 

 

4) 사용자정의 인터페이스의 구현체 클래스를 만든다. 

(여기에서 본격적인 QueryDSL 로직을 작성한다. )

 

💻 MemberRepositoryImpl

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom{

    private final JPAQueryFactory queryFactory;

    @Override
    public List<MemberTeamDto> search(SearchCond cond) {

        List<MemberTeamDto> result = queryFactory
                .select(new QMemberTeamDto(
                        member.id.as("memberId"),
                        member.age,
                        member.username,
                        team.name.as("teamName")
                ))
                .from(member)
                .leftJoin(member.team , team)
                .where(
                        usernameEq(cond.getUsername()),
                        teamNameEq(cond.getTeamName()),
                        ageLoe(cond.getAgeLoe()),
                        ageGoe(cond.getAgeGoe())
                )
                .fetch();

        return result;

    }
    
    private BooleanExpression ageGoe(Integer ageGoe) {
        return ageGoe != null ? member.age.goe(ageGoe) : null;
    }

    private BooleanExpression ageLoe(Integer ageLoe) {
        return ageLoe != null ? member.age.goe(ageLoe):null;
    }

    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName):null;
    }

    private BooleanExpression usernameEq(String username) {
        return hasText(username) ? member.username.eq(username):null;
    }


}

 

 

⭐⭐⭐⭐⭐

1) 구현하는 클래스 (MemberRepositoryImpl) 
2) 사용자 정의 인터페이스 (MemberRepositoryCustom)
3) 상속받아 해당 클래스를 사용하게 되는 레파지토리 (MemberRepository)

* 1, 3의 이름이 일치해야 스프링이 인식해 관리해준다.

 

 

 


 

 

++ 추가 학습

 

우아콘 2020

 

워밍업

 

1) extends / implements 사용하지 않기

 

 

일반적으로 QueryDSL을 사용한다고 하면, JpaRepositoy와 별도interface를 상속을 받음

그리고 해당 인터페이스를 구현하는 구현체 Impl가 필요하다. 

-- 매번 interface와 / Impl 구조가 필요함. ((과함))

.>> 이때 Querydsl RepositorySupprot 를 상속받고, super생성자에 Entity를 등록하는 것도 있음 (이것도 불편함)

 

꼭 무언가를 상속/구현 , Entity를 지정하지 않더라도

쿼리DSL을 사용할 수 있는 방법은?

 

>> JPAQueryFactory만 있으면 Querydsl은 사용가능하다.

 

실제 querydsl을 사용할 땐 Factory만 있어서 생성자 주입받아서 사용하면, 모든 기능을 사용할 수 있어서

상속 구현 다 제거해도 됨.

 

 

 

2) 동적쿼리는 BooleanExpression

일반적으로 동적쿼리는 BooleanBuilder를 사용한다.

 

if문이 3개뿐이지만, 컬럼이 7개 10개가 되면 한 화면을 꽉 채우게 됨

어떤 쿼리인지 예상하기도 어려움

 

그래서 BooleanExpression을 많이 쓰는데,

메소드를 만들어, null로 리턴을 받을경우 자동으로 종료를 시킴

-- 모든 조건이 null이면 조건문이 다 삭제되어서 좋데

 

 

 

성능개선 select

1) exist 메소드 금지

 

 

이해하기 어려우니 그만해야겠다 기본이나 잘하고 성능개선을 보자 ㅎㅎ

 

 


 

references