728x90

QueryDSL

Criteria의 단점 너무 복잡하고 어렵다는 것 그래서 JPQL이 어떻게 생성되는지 파악이 어렵다.
그래서 나온게 이 QueryDSL이다.. 코드로 작성하는데 간결하고 알아보기 쉽다.

QueryDSL은 오픈소스 프로젝트이다. 단순 CRUD보다는 이름에 걸맞게 데이터를 조회 그러니까 통계형 쿼리를 짤때 적합하지 않을까 생각한다.

QueryDSL Setting

build.gradle

buildscript {
    ext {
        ...
        querydslVersion = '1.0.10'
    }

    dependencies {
        ....
        classpath "gradle.plugin.com.ewerk.gradle.plugins:querydsl-plugin:$querydslVersion"
    }
}

subprojects {
    apply plugin: 'com.ewerk.gradle.plugins.querydsl'

    ext {
        querydslDir = "$buildDir/generated/querydsl"
    }

    dependencies {
        ...
        implementation 'com.querydsl:querydsl-jpa'
    }

    querydsl {
        jpa = true
        querydslSourcesDir = querydslDir
    }

    sourceSets {
        main.java.srcDir querydslDir
    }

    configurations {
        querydsl.extendsFrom compileClasspath
    }

    compileQuerydsl {
        options.annotationProcessorPath = configurations.querydsl
    }
}

이렇게 설정을 해주었는데 중요한 부분은 subproject.ext.querydslDir 부분이다.
$buildDir이 뜻하는 것은 우리의 스터디는 일단 모듈을 나누어서 한사람당 모듈을 사용하고 있다.
그래서 의존성을 구분해놓았는데 여기서의 $buildDir은 모듈의 빌드된 폴더
build/를 의미하며 build/generated/querydsl 폴더에 Entity클래스 앞에 Q가 붙은 클래스가 빌드되어 있다.
이것으로 QueryDSL을 세팅해주는 것이다.

QuerydslConfig.java

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

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

전에 말했듯 JavaEE환경에서는 @PersistenceContext를 활성화하면 알아서 주입받는다.
JPAQueryFactory 가 QueryDSL을 사용하기 위해서 구현해야 하는 것이다.

이렇게 설정해주면 QueryDSL을 사용할 수가 있다.

이제 테스트 코드를 작성해 보자.

@DataJpaTest
public class QuerydslTest {
    @PersistenceUnit
    EntityManagerFactory emf;
    EntityManager em;
    EntityTransaction tx;
    JPAQueryFactory jpaQueryFactory;

    private Member member;

    @BeforeEach
    void setUp() {
        em = emf.createEntityManager();
        tx = em.getTransaction();
        jpaQueryFactory = new JPAQueryFactory(em);
        tx.begin();
        Address address = Address.builder()
                .city("city")
                .street("street")
                .zipcode("zipcode")
                .build();

        Address comAddress = Address.builder()
                .city("cocity")
                .street("costreet")
                .zipcode("cozipcode")
                .build();

        Period period = Period.of("20210714", "20210714");

        member = Member.builder()
                .name("kim")
                .age(26)
                .period(period)
                .homeAddress(address)
                .companyAddress(comAddress)
                .build();

        em.persist(member);
        em.flush();
    }

    @Test
    @DisplayName("QueryDSL 시작")
    void querydslTest() {
        QMember qMember = new QMember("m"); //생성된 별칭
        List<Member> members = jpaQueryFactory.selectFrom(qMember)
                .where(qMember.name.eq("kim")).orderBy(qMember.id.desc()).fetch();

        assertThat(members.get(0).getName()).isEqualTo("kim");
        assertThat(members.get(0).getAge()).isEqualTo(26);
    }
}

책에서는 orderBy(qMember.id.desc()).list(qMember); 로 사용했는데
버전이 바뀌면서 list 메소드는 없어졌다고 한다.
그래서 공식문서를 찾아보니까

QueryDSL로 select 조회 쿼리를 만들었을때 로그이다.

이상한점 이라고 느낄수 있다면 어? 하고 코드상에는 jpaQueryFactory.from으로 시작하기 때문에 이것이 어떤 CRUD인지 모를 수 있다.fetch()를 해주면 반환이 List인데 그래도 타입은 캐스팅을 해주어야 한다.
자동으로 뭔가 Q클래스를 맞춰줄줄 알았다.

🤣

ㅋㅋㅋㅋㅋㅋㅋ 아니었다..... 그냥 기본값이 select가 아니라 list(qMember) 를 해주던 버전에서는 이 list 메소드가 엔티티 타입을 맞춰서 List에 넣어주었는데 지금은 selectFrom(qMember)를 해주면 이것이 List 타입을 맞춰준다.
이렇게 또 하나 깨달음을 얻는다 👍

페이징 정렬

@Test
void 페이징_테스트() {
    QItem item = QItem.item;
    List<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                                           .orderBy(item.price.desc(), item.name.desc())
                                           .offset(1).limit(3).fetch();

    result.forEach(r -> System.out.println(r.toString()));
}

여기서 where 조건에 lt는 부등호로 < 이고, gt는 >이다. orderBy절에서는 쿼리 타입인(Q)에서 asc(), desc()를 지원해준다. 페이징은 offsetlimit 을 조합해서 사용하면 된다.
이렇게해서 얻은 결과이다.

전체 데이터 수를 알고 싶을때는

listResults()를 사용한다.

이것 역시 바뀌었다.
SearchResults<T> 가 아니라 버전이 바뀌면서 QueryResults<T>로 변경되었다.

listResults() 가 아니라 fetchResults()로 바뀌게 되었다.

@Test
@DisplayName("조회 결과 테스트")
void listResultsTest() {
    QueryResults<Item> result = jpaQueryFactory.selectFrom(item).where(item.price.lt(500))
                .orderBy(item.price.desc(), item.name.desc())
                .offset(1).limit(3).fetchResults();

    long total = result.getTotal(); //500보다 작은수 총 count
    long limit = result.getLimit(); //limit 3
    long offset = result.getOffset(); // offset 1
    List<Item> results = result.getResults();

    assertThat(total).isEqualTo(4L);
    assertThat(limit).isEqualTo(3L);
    assertThat(offset).isEqualTo(1L);

    results.forEach(r -> System.out.println(r.toString()));
}

getTotal은 오류의 여지가 있어보인다. 왜냐면 저 QueryDSL 전체를 시켰을때의 count가 아니라 count가 먼저 실행되기 때문에 where() 조건까지 수행한 count가 나온다.

결과의 조회는 QueryResultsgetResults()를 사용하여 페이징 정렬할때처럼의 결과를 얻을수가 있다.

그룹

그룹은 groupBy를 사용하고 그 다음에 조건을 해주려면 having을 사용하면 된다.

List<Item> results = jpaQueryFactory.selectFrom(item)
                                    .groupBy(item.price)
                                    .having(item.price.lt(500))
                                    .fetch();

728x90

+ Recent posts