JPA - QueryDSL, JPQL Query Builder Class

2019. 2. 11. 01:07Web/JPA

JPA - QueryDSL, JPQL Query Builder Class



이전 포스팅에서 다뤘던 JPA Criteria는 문자가 아닌 코드로 JPQL을 작성하도록 도와주는 쿼리빌더였기 때문에 문법 오류를 컴파일 단계에서 잡을 수 있고 IDE툴을 이용하여 자동완성 기능의 도움을 받을 수 있기 때문에 여러가지 장점이 있었다. 하지만 Criteria의 가장 큰 단점은 코드양이 줄지도 않고 너무 복잡하며 바로바로 쿼리를 예상하기가 쉽지 않았다. 하지만 이번에 다룰 QueryDSL은 Criteria와 같이 문자가 아닌 코드로 쿼리를 작성하며, 쉽고 간결하고 빌더로 작성된 모양도 쿼리와 비슷해 쉽게 예상이 가능하다. 오히려 Criteria보다 사용하기 편하다. 하지만 큰 문제라고는 할 수 없지만 Criteria는 JPA가 지원하는 표준이고, QueryDSL은 표준은 아니고 그냥 오픈소스이다. 


▶︎▶︎▶︎JPA - Criteria Query(객체지향 쿼리 빌더), JPQL Query Builder Class

▶︎▶︎▶︎QueryDSL Reference 번역 By 최범균

▶︎▶︎▶︎JPQL(객체지향쿼리),Java Persistence Query Language

QueryDSL이 지원하는 프로젝트

-QueryDSL은 처음에 HQL(Hibernate Query Language)을 코드로 작성할 수 있도록 해주는 프로젝트로 시작해서 지금은 JPA, JDO, JDBC, Lucene, Hibernate Search, 몽고DB, 자바 컬렉션 등을 다양하게 지원하고 있는 오픈소스이다. 이름 그대로 데이터를 조회하는 데에 기능이 아주 특화되어있다.




QueryDSL 설정


필요라이브러리

1
2
3
4
5
6
7
8
9
10
11
12
<!-- QueryDSL Library -->
<dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
        <version>3.6.3</version>
</dependency>
<dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <version>3.6.3</version>
        <scope>provided</scope>
</dependency>
cs


환경설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- QueryDSL Plugin -->
<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.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
        </execution>
   </executions>
</plugin>
cs







혹시 프로젝트에 에러가 있다면 target/generated-sources/java가 클래스패스에 등록되어있는지 확인해보면 될듯싶다. 대게 IDE가 자동으로 해주지만 안되는 경우도 있지 않을까 생각한다.




QueryDSL에서 사용되는 객체설명 - JPAQuery, Q클래스


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void intro() {
        
        //JPAQuery 객체를 생성한다. 생성자 매개변수로 EntityManager인스턴스 전달
        JPAQuery query = new JPAQuery(em);
        
        //조회의 시작점인 엔티티를 Q_.class로 생성해준다. 생성자로 별칭을 넘겨준다.
        QMemberJPQL member = new QMemberJPQL("m");
        
        /*만약 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용하는 것이 아니라면
        QMemberJPQL member = QMemberJPQL.memberJPQL 로 생성해도 위와 동일하다.
        만약 위와같이 기본 인스턴스를 사용한다면 클래스 package밑에 
        import static ~.QMemberJPQL.memberJPQL로 임포트해놓고 사용하면 더 간결해진다.*/
        
        List<MemberJPQL> members = query.from(member)
                                        .where(member.username.eq("여성게1"))
                                        .orderBy(member.age.desc())
                                        .list(member); //매개변수에 조회할 프로젝션을 작성한다.
        
        System.out.println("=====================intro======================");
        
        for(MemberJPQL m : members) {
                System.out.println(m.toString());
        }
        
        System.out.println("=====================intro======================");
}
cs


JPAQuery는 빌더패턴으로 된 클래스로써 쿼리 메소드들을 계속 해서 호출하고 있다. 결국에 이 객체가 나중에 JPQL 쿼리로 변환이 되는 것이다. 위의 소스로 간단히 JPQL과 비교를 해보면,


 JPQL

QueryDSL 

 select m from MemberJPQL m .... 

 QMemberJPQL member = new QMemberJPQL("m");

 select m ....

.list(member) 

 from MemberJPQL m

.from(member) 

 where m.username = ?1 

.where(member.username.eq("여성게1") 

 order by m.age desc 

.orderBy(member.age.desc()) 


표시에서도 보이듯이 자바코드로 변환하였지만 JPQL과 비교하여 이질감이 없다. Criteria보다 쉽고 간결하고 이해하기 쉬운 코드로 작성이 가능하다.



검색조건쿼리


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
 * 검색조건 쿼리
 */
public void searchCondition() {
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        
        //and,or을 사용할 수 있다. 또한 ","구분자로 나열해도 된다(and)
        /*
         * between(A,B) - 사이값
         * contains(A) - SQL에서 like '%A%'와 같다.
         * startWith(A) - SQL에서 like 'A%'와 같다.
         * 
         * 이렇게 다양한 메소드를 제공해준다.
         */
        List<MemberJPQL> members = query.from(qMember)
                                    .where(qMember.username.contains("여성게").and(qMember.age.gt(36)))
                                    .list(qMember);
        
        System.out.println("=====================searchCondition======================");
        
        for(MemberJPQL m : members) {
                System.out.println(m.toString());
        }
        
        System.out.println("=====================searchCondition======================");
}
cs


QueryDSL의 where 절에는 and 나 or을 사용할 수 있다. 그리고 쿼리 타입의 필드는 비교연산을 위한 대부분의 메소드를 명시적으로 제공한다. 즉, IDE가 제공하는 자동완성 기능을 도움받아 문법오류없는 쿼리 작성이 쉽게 가능하다!




결과 조회 - uniqueResult(), singleResult(), list()



.uniqueResult() - 조회 결과가 한 건일 때 사용한다. 조회 결과가 없으면 null을 반환하고 결과가 하나 이상이면 

  com.mysema.query.NonUniqueResultException 예외가 발생한다.

  

.singleResult() - uniqueResult()와 같지만 결과가 하나 이상이면 처음 데이터를 반환한다.

 

.list() - 결과가 하나 이상일 때 사용한다. 결과가 없으면 빈 컬렉션을 반환한다.



페이징과 정렬



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
 * 페이징과 정렬
 */
public void pagingAndOrderBy() {
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        
        /*List<MemberJPQL> members = query.from(qMember)
                                          .where(qMember.age.gt(30))
                                          .orderBy(qMember.age.desc())
                                          .offset(0).limit(2)
                                          .list(qMember);*/
        
        /*      
         * 위와 동일한 기능이다.
         * QueryModifiers queryModifiers = new QueryModifiers(2L,0L); limit,offset      
         * List<MemberJPQL> members = query.from(qMember)
                                        .where(qMember.age.gt(30))
                                        .orderBy(qMember.age.desc())
                                        .restrict(queryModifiers)
                                        .list(qMember);
        */
        
        /* 하지만 실제 페이징 처리를 하려면 검색된 전체 데이터수를 알아야한다. 이때는 list() 대신 listResults()를 사용한다.
          listResults()를 사용하면 전체 데이터 조회를 위한 count 쿼리를 한번더 실행한다.*/
          
          SearchResults<MemberJPQL> result = query.from(qMember)
                                                  .where(qMember.age.gt(30))
                                                  .orderBy(qMember.age.desc())
                                                  .offset(0).limit(2)
                                                  .listResults(qMember);
                                                                        
          long totalNum = result.getTotal(); //검색된 전체데이터수
          long limitNum = result.getLimit();
          long offsetNum = result.getOffset();
 
          //조회된 데이터(전체 데이터가 아닌 위의 리밋과 오프셋을 이용하여 조회한 데이터)
          List<MemberJPQL> members = result.getResults(); 
        
        
        System.out.println("=====================pagingAndOrderBy======================");
        System.out.println("전체 데이터수:"+totalNum +",한 페이지당 글수:"+limitNum +",시작글 번호:"+offsetNum);
        for(MemberJPQL m : members) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================pagingAndOrderBy======================");
        
}
cs


코드만 봐도 직관적으로 무슨 코드인지 알 수 있다.



집합



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void groupAndHaving() {
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        
        List<MemberJPQL> members = query.from(qMember)
                                        .groupBy(qMember.username)
                                        .having(qMember.age.gt(36))
                                        .list(qMember);
        
        System.out.println("=====================groupAndHaving======================");
        for(MemberJPQL m : members) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================groupAndHaving======================");
}
cs



조인



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public void standardJoin() {
        
        JPAQuery query = new JPAQuery(em);
        
        QOrderJPQL qOrder = QOrderJPQL.orderJPQL;
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        QProductJPQL qProduct = QProductJPQL.productJPQL;
        
        List<OrderJPQL> orders = query.from(qOrder)
                                      .where(qOrder.member.username.eq("여성게1"))
                                      .join(qOrder.member,qMember)
                                      .leftJoin(qOrder.product,qProduct)
                                      .list(qOrder);
        
        System.out.println("=====================standardJoin======================");
        for(OrderJPQL m : orders) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================standardJoin======================");
}
 
public void joinOn() {
        JPAQuery query = new JPAQuery(em);
        QOrderJPQL qOrder = QOrderJPQL.orderJPQL;
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        QProductJPQL qProduct = QProductJPQL.productJPQL;
        
        List<OrderJPQL> orders = query.from(qOrder)
                          .leftJoin(qOrder.member,qMember)
                          .on(qOrder.product.name.eq("맥북1"))
                          .list(qOrder);
 
        System.out.println("=====================joinOn======================");
        for(OrderJPQL m : orders) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================joinOn======================");
        
}
 
public void fetchJoin() {
        JPAQuery query = new JPAQuery(em);
        QOrderJPQL qOrder = QOrderJPQL.orderJPQL;
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        QProductJPQL qProduct = QProductJPQL.productJPQL;
        
        /*
         * join on 절에는 패치조인 불가
         */
        List<OrderJPQL> orders = query.from(qOrder)
                          .leftJoin(qOrder.member,qMember).fetch()
                          .list(qOrder);
 
        System.out.println("=====================fetchJoin======================");
        for(OrderJPQL m : orders) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================fetchJoin======================");
}
 
public void thetaJoin() {
        JPAQuery query = new JPAQuery(em);
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        QOrderJPQL qOrder = QOrderJPQL.orderJPQL;
        List<OrderJPQL> orders = query.from(qMember,qOrder)
                                      .where(qOrder.member.eq(qMember))
                                      .list(qOrder);
 
        System.out.println("=====================thetaJoin======================");
        for(OrderJPQL m : orders) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================thetaJoin======================");
}
cs


조인은 내부조인,외부조인 등을 사용할 수 있고 join on과 성능 최적화를 위한 fetch 조인도 사용할 수 있다. QueryDSL 조인의 기본 문법은 우선 조인에 사용될 쿼리타입 클래스들을 모두 생성해준다. 그리고 join메소드 파라미터 첫번째에는 조인 대상(Order의 연관필드), 두번째 파라미터에 별칭으로 사용할 쿼리타입(Q클래스)을 지정하면 된다.



서브쿼리 - JPASubQuery 객체



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/*
 * 서브쿼리
 */
public void subQuery() {
        
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        QMemberJPQL subMember = new QMemberJPQL("subMember");
        
        List<MemberJPQL> results = query.from(qMember)
                                        .where(qMember.age.eq(
                                        new JPASubQuery()
                                                .from(subMember)
                                                .unique(subMember.age.max())
                                        )).list(qMember);
        
        System.out.println("=====================subQuery======================");
        for(MemberJPQL m : results) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================subQuery======================");
}
 
/*
 * 서브쿼리
 */
public void subQuery2() {
        
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        QMemberJPQL subMember = new QMemberJPQL("subMember");
        
        List<MemberJPQL> results = query.from(qMember)
                                        .where(qMember.in(
                                        new JPASubQuery()
                                                .from(subMember)
                                                .where(subMember.username.in("여성게1","여성게3"))
                                                .list(subMember)
                                        )).list(qMember);
        
        System.out.println("=====================subQuery2======================");
        for(MemberJPQL m : results) {
        System.out.println(m.toString());
        }
        
        System.out.println("=====================subQuery2======================");
}
cs


서브쿼리는 JPASubQuery 객체를 생성하여 사용한다. 만약 서브 쿼리의 결과가 아나면 unique(),여러건이면 list()를 사용할 수 있다.



프로젝션과 결과 반환


반환 결과가 여러건 일경우 2가지의 방법이 있다. 첫번째는 튜플을 이용하는 방법이고 두번째는 DTO클래스를 이용하여 반환받는 방법이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* 
 * 반환결과가 여러건일 경우(튜플타입사용)
 */
public void tupleType() {
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        
        com.mysema.query.Tuple tuple = query.from(qMember)
                                            .where(qMember.username.eq("여성게1"))
                                            .singleResult(qMember.username,qMember.age); 
                                                        //프로젝션은 여기다가 작성.
        
        System.out.println("=====================tupleType======================");
        
        System.out.println(tuple.get(qMember.username)+" "+tuple.get(qMember.age));
        
        System.out.println("=====================tupleType======================");
}
 
/*
 * 반환결과가 여러건일 경우(JPQL의 NEW명령어와 같이 DTO사용)
 * 
 * 1.Projections.bean() -> setter이용
 * 2.Projections.fields() -> 필드직접 접근.(reflection으로 접근하므로 private이여도 접근가능)
 * 3.Projections.constructor() -> 생성자로 접근, 생성자 매개변수와 순서가 동일해야함.
 */
public void nonTuple() {
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        
        //1
        MemberDTO dto = query.from(qMember)
                         .where(qMember.username.eq("여성게1"))
                         .singleResult
                         (Projections.bean(MemberDTO.class, qMember.username, qMember.age.as("userage")));
                                //프로젝션은 여기다가 작성. 만약 dto와 엔티티의 필드명이 다르다면 "as"메서드이용.
        /*
         * 2.~Projections.fields(MemberDTO.class, qMember.username, qMember.age.as("userage")));
         * 3.~Projections.constructor(MemberDTO.class, qMember.username, qMember.age)); 
                ->해당 타입의 매개변수를 들어가는 것뿐이니까, 필드명을 맞춰줄 필요 없음
         */
        System.out.println("=====================nonTuple======================");
        
        System.out.println(dto.toString());
        
        System.out.println("=====================nonTuple======================");
}
cs



DISTINCT



query.distinct().from(.....)으로 작성하면 된다.




수정, 삭제 배치 쿼리 - JPAUpdateClause, JPADeleteClause



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
 * 수정,삭제 배치 쿼리
 */
public void batchQuery() {
        JPAQuery query = new JPAQuery(em);
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        tx.begin();
        JPAUpdateClause updateBatch = new JPAUpdateClause(em, qMember);
        long count = updateBatch.where(qMember.username.eq("여성게1")).set(qMember.username,"바보멍청이").execute();
        
        /*JPADeleteClause deleteBatch = new JPADeleteClause(em, qMember);
        long count = deleteBatch.where(qMember.username.eq("여성게1")).execute();*/
        
        tx.commit();
}
cs


트랜잭션 선언은 필수이다.



동적쿼리 - BooleanBuilder



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
 * 동적쿼리
 */
public void dynamicQuery(String name,String teamName) {
        
        JPAQuery query = new JPAQuery(em);
        
        QMemberJPQL qMember = QMemberJPQL.memberJPQL;
        
        BooleanBuilder builder = new BooleanBuilder();
        if(!StringUtils.isEmpty(name) && !StringUtils.isEmpty(teamName)) {
                builder.and(qMember.username.eq(name));
                builder.and(qMember.team.name.eq(teamName));
        }
        
        MemberJPQL member = query.from(qMember).join(qMember.team).where(builder).uniqueResult(qMember);
        
        if(!ObjectUtils.isEmpty(member)) {
                System.out.println("=====================dynamicQuery======================");
                System.out.println(member.toString());
                System.out.println("=====================dynamicQuery======================");
        }
}
cs


BooleanBuilder를 이용하여 가변적인 파라미터에 따른 동적쿼리를 작성하였습니다.





사실 뭐가 편하고 뭐가 좋다라는 것은 개인적인 견해가 많이 들어가는 것도 있습니다. 그래서 저는 개인적으로 Criteria보다는 QueryDSL이 편하다고한 것이구요. 만약 복잡한 것을 더 편하게 생각하시는 분들고 계시기 때문에 제가 포스팅한 Criteria와 QueryDSL을 참고해보시고 더 편하신 것을 사용하는 것이 좋을것 같습니다.