tutorial,

QueryDSL Example

문병천 Linkedin Sep 11, 2019 · 6 mins read
QueryDSL Example
Share this

이번에는 JPA 에서 한 발자국 더 옮겨 보자.

Unified Queries for Java.Querydsl is compact, safe and easy to learn. -http://www.querydsl.com

QueryDSL 의 장점은

  • code completion in IDE (코드를 작성하듯 IDE에서 쉽게 작성할수 있다. 오타로 인한 스트레스가 적다.)
  • almost none syntactically invalid queries allowed (구문 오류가 거의 나지 않는다.)
  • domain types and properties can be referenced safely (도메인 참조를 안전하게 할 수 있다.)
  • adopts better to refactoring changes in domain types (도메인 유형의 리펙토링 적용이 좀더 유연하다.)

종합 해보면 작성하기 쉽고 쿼리를 안전하게 코드로 만들수 있다는 의미이다. 일단 한번 해보자!

1. 기존 프로젝트를 수정하자

QueryDSL 라이브러리를 추가 하기 위해 maven의 pom.xml 파일을 수정하자.

<properties>
    <java.version>1.8</java.version>
    <querydsl.version>4.2.1</querydsl.version>
</properties>

 <!-- QueryDSL -->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>${querydsl.version}</version>
</dependency>
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>${querydsl.version}</version>
</dependency>

위의 Dependency 태그를 Dependencies 태그 안에 추가 한다.

<plugin>
    <groupId>com.mysema.maven</groupId>
    <artifactId>apt-maven-plugin</artifactId>
    <version>1.1.3</version>
    <executions>
        <execution>
            <goals>
                <goal>process</goal>
            </goals>
            <configuration>
                <outputDirectory>target/generated-sources/java</outputDirectory>
                <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
        </execution>
    </executions>
</plugin>

위의 plugin 태그는 plugins 태그 안에 추가를 한다. QueryDSL 에서 maven 컴파일시 도메인 Entity 를 찾아서 컴파일 해주는 옵션이다.

프로젝트의 Repository 패키지를 삭제 하자. JPQL문은 이제 그만 보내주자.

2. Maven Compile

QueryDSL 을 사용하기 위해서는 도메인 Entity 클래스를 컴파일 해주어야 한다. 결과물로 생기는 Q클래스를 이용하여 Query를 작성한다.

mvn compile

위의 명령을 터미널을 사용하여 프로젝트 루트 폴더(pom.xml 이 존재하는 곳)에 입력하거나

mvncompile

IDE 도구를 사용하여 compile 한다. 그러면 결과물이

mvn컴파일후QEntitiy확인

짠 하고 생성 되었을 것이다.

3. Test Code

이제 모든 준비는 끝났다. 테스트 코드를 작성하여 보자. 기존의 JPA코드는 삭제하고 밑의 코드를 작성하여 본다.

@PersistenceContext
private EntityManager entityManager;

// 조인 테스트
@Test
public void testQueryDSL(){
    String userId = "1"; //문병천을 검색해보겠습니다.
    QUserRole qUserRole = QUserRole.userRole;
    QUser qUser = QUser.user;
    JPAQuery<?> query = new JPAQuery<>(entityManager);
    List<User> user = query.select(qUser)
            .from(qUser)
            .innerJoin(qUser.userRole, qUserRole)
            .where(qUser.user_id.eq(userId)).orderBy(qUser.name.desc()).fetch();
    logger.info("UserRole List : " + user);
}

User 테이블과 UserRole 테이블을 조인하고 User의 이름순으로 정렬한 쿼리이다. 이너 조인이다. 이 외에도 Left Join, Right Join, Join 함수가 있다.

// 다이나믹 쿼리 검색 조건 테스트 1 Boolean Builder
@Test
public void testQueryDSLDynamic1(){
    QUser qUser = QUser.user;
    BooleanBuilder builder = new BooleanBuilder();
    JPAQuery<?> query = new JPAQuery<>(entityManager);
    //String name = null;
    String name = "홍길동";
    //String email = "munbc@mz.co.kr";
    String email = null;

    if(!StringUtils.isEmpty(name)){
        builder.and(qUser.name.eq(name));
    }

    if(!StringUtils.isEmpty(email)){
        builder.and(qUser.email.eq(email));
    }

    User user = query.select(qUser).from(qUser).where(builder).fetchOne();
    logger.info("Dynamic Query Result 1 : " + user);
}

위의 테스트 코드는 검색 조건 부분에 해당 값이 있으면 조건에 들어가는 다이나믹 쿼리를 테스트 한 것이다. 주석 처리된 name 과 email 쪽을 수정하면서 로그에 찍히는 쿼리의 where 부분을 살펴 보자. BooleanBuilder는 and, or, not 등등 조건문에 필요한 함수들을 가지고 있다.

// 다이나믹 쿼리 검색 조건 테스트 2 Boolean Expression
@Test
public void testQueryDSLDynamic2(){
    QUser qUser = QUser.user;
    JPAQuery<?> query = new JPAQuery<>(entityManager);
    String name = null;
    //String name = "홍길동";
    String email = "honggil@korea.com";
    //String email = null;
    String address = null;
    //String address = "주소4";

    User user = query.select(qUser).from(qUser).where(eqName(name), eqEmail(email), eqAddress(address)).fetchOne();
    logger.info("Dynamic Query Result : 2 " + user);
}

private BooleanExpression eqName(String name){
    if(StringUtils.isEmpty(name)){
        return null;
    }
    return QUser.user.name.eq(name);
}

private BooleanExpression eqEmail(String email){
    if(StringUtils.isEmpty(email)){
        return null;
    }
    return QUser.user.email.eq(email);
}

private BooleanExpression eqAddress(String address){
    if(StringUtils.isEmpty(address)){
        return null;
    }
    return QUser.user.address.eq(address);
}

위의 테스트 코드는 다른 방식으로 다이나믹 쿼리를 사용해 보았다. BooleanExpression 이라는 클래스를 이용하여 where 절에 조건을 넣을 수 있게 하였다.

// 서브쿼리 테스트 1
@Test
public void testQueryDSLSubQuery(){
    QUser qUser = QUser.user;
    QUser u = new QUser("u");
    JPAQuery<?> query = new JPAQuery<>(entityManager);
    List<Tuple> users = query.select(qUser,
    ExpressionUtils.as(JPAExpressions.select(qUser.name.count()).from(qUser).where(qUser.created_at.before(new Date())),
    "UserCount"
    )).from(qUser).fetch();
    logger.info("User List : " + users);
    for(Tuple user : users){
        logger.info("user : " + user.get(1, Long.class));
    }
}

이번엔 서브 쿼리를 만들어 보았다. User 정보에 추가로 현재시간 이전에 만들어진 User의 총 카운트 수를 추가하여 보았다. 이 경우는 User 도메인과 정보가 다르기 때문에 Tuple이라는 클래스를 이용하여 결과를 받을 수 있었다. as 함수를 사용하여 컬럼 명(UserCount)이 나오게 하고 싶었지만 실제 결과에서는 보이지는 않았다. Tuple에서 값을 꺼낼 때는 값의 index와 타입클래스를 정확이 알아야한다. 주의 하자.

여기까지 QueryDSL 에 대하여 간단한 Tutorial을 진행해 보았다. 서비스 로직에 맞춰 DB 쿼리 부터 짜던 시대는 지난거 같다. 쿼리를 코딩하듯 짜는 시대가 온거 같다. 그리고 코딩이 편리한 반면 도메인을 추가하거나 변경 할 때 마다 컴파일하는 과정이 필요한 부분은 좀 불편하게 다가왔다.

각자 한번 사용해 보고 어떤 장점과 단점이 있는지 느껴 보았으면 한다.

Written by 문병천
그냥저냥 이것저것 모아두는 습성을 가졌습니다.