Web/JPA 2019. 2. 12. 00:19

JPA - 영속성 컨텍스트와 JPQL



쿼리 후 영속 상태인 것과 아닌것

-JPQL의 조회 대상은 엔티티, 임베디드 타입, 값 타입 같이 다양하다. 하지만 엔티티를 조회하면 영속성 컨텍스트에 관리되지만 나머지는 관리되지 않는다. 예를 들어 임베디드 타입만 참조해서 조회했을 경우에 값을 변경해서 플러시 시점에 반영되지 않는다. 하지만 엔티티를 조회하여 엔티티가 가지고 있는 임베디드타입을 변경했을 경우에는 플러시 시점에 변경이 반영이 된다.



JPQL로 조회한 엔티티와 영속성 컨텍스트

-JPQL로 데이터베이스에서 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 JPQL로 데이터베이스에서 조회한 결과를 버리고 대신에 영속성 컨텍스트에 있던 엔티티를 반환한다. 이때 식별자 값을 이용하여 비교한다.


여기서 동일하다는 것은 엔티티의 모든 필드값이 동일하다는 것을 의미하는 것이 아닌 엔티티의 식별자값이 동일함을 의미한다. 즉, 만약 1차 캐시의 member1의 이름이 "여성게"이고 , 디비에 있는 member1의 이름이 "소라게"여도 식별자 값이 같음으로 "여성게"로 조회가 되게 된다. (물론 이런 상황 자체가 있는 것이 잘못된 것이긴함) 하지만 만약 영속성컨텍스트에 디비에서 조회한 엔티티가 존재하지 않으면 디비에서 가져온 엔티티를 영속성 컨텍스트에 넣는다. 왜 그런가?


1. 새로운 엔티티를 영속성 컨텍스트에 하나 더 추가한다. -> 식별자값은 유일함으로 이것은 말이안됨.

2.기존 엔티티를 새로 검색한 엔티티로 대체한다. -> 그럴싸 하지만 만약 엔티티가 수정중에 있다면 아주 큰문제가됨.

3.기존 엔티티는 그대로 두고 새로 검색한 엔티티를 버린다. -> 이 규칙을 따르게 되는 것이다. 영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장해야하기 때문 




find() vs JPQL

-em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다. 따라서 해당 엔티티가 메모리에 존재한다면(이미 이전에 동일한 엔티티를 조회한 적이 있기때문) 성능상 이점이 있다.

JPQL은 항상 디비에서 찾는다. 왜냐하면 JPQL은 SQL로 변환되어 데이터베이스에 바로 날려보기 때문이다. 항상 디비에서 찾지만 디비에서 찾은 엔티티가 영속성 컨텍스트에 존재한다면 디비에서 찾은 엔티티를 버리고 영속성 컨텍스트에 있는 것을 반환값으로 넘긴다.



JPQL과 플러시 모드

-플러시는 영속성 컨텍스트의 변경 내역을 디비에 동기화하는 것이다. JPA는 플러시가 일어날 때 영속성 컨텍스트에 등록, 수정, 삭제한 엔티티를 찾아서 적절한 SQL을 만들어 디비에 반영한다.

플러시 모드
1)FlushModeType.AUTO : 커밋 또는 쿼리 실행 시 플러시(기본값)
2)FlushModeType.COMMIT : 커밋시에만 플러시


쿼리와 플러시모드

JPQL은 영속성 컨텍스트에 있는 데이터를 고려하지 않고 디비에서 데이터를 조회한다. 따라서 JPQL을 실행하기 전에 영속성 컨텍스트의 내용을 디비에 반영해야한다. 그렇지 않으면 의도하지 않은 결과가 발생할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
               
     tx.commit();
     if(count == 1) {
             MemberJPQL member = query.from(qMember)
                                      .where(qMember.username.eq("바보멍청이")).uniqueResult(qMember);
            System.out.println("=====================batchQuery======================");
             if(!ObjectUtils.isEmpty(member)) {
                     System.out.println(member.toString());
             }
             System.out.println("=====================batchQuery======================");
     }
}
cs


이 예제는 QueryDSL의 수정 배치 쿼리이다. 이 예제의 결과가 어떻게 될것인가? 결과는 정말 의도치 않게 나오게 된다. QueryDSL의 배치쿼리는 영속성컨텍스트와 관계없이 직접 디비로 요청을 보내게 된다. 이러는 순간 디비와 영속성컨텍스트의 동기화는 깨진것이다. 변경을 완료했고 나는 변경된 이름으로 쿼리를 날렸는데, 이게 뭐람 변경전의 "여성게1"이란 결과가 나오게 된다. 이유는 위에서 설명한 것과 같이 영속성컨텍스트에 이미 존재하기 때문에 디비에서 가져온 엔티티가 데이터가 달라도 식별자 값이 같기 때문에 버려지게 되고 수정전에 이름이 출력되게 되는 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void batchQuery2() {
        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();
        MemberJPQL member2 = query.from(qMember)
                                .where(qMember.username.eq("바보멍청이")).uniqueResult(qMember);
        member2.setUsername("바보멍청이");
        
        tx.commit();
        if(count == 1) {
                
                MemberJPQL member = new JPAQuery(em).from(qMember)
                                    .where(qMember.username.eq("바보멍청이")).uniqueResult(qMember);
                System.out.println("=====================batchQuery======================");
                if(!ObjectUtils.isEmpty(member)) {
                        System.out.println(member.toString());
                }
                System.out.println("=====================batchQuery======================");
        }
        
}
cs


이 코드는 실무에서는 사용할 수없는 코드이다. 하지만 직관적으로 영속성 컨텍스트의 값을 수정배치쿼리와 동일하게 변경한것을 보여주기 위해 짠것이다. 이 예제의 결과는 수정배치에서 변경된 값과 동일한 엔티티가 반환되게 된다.

posted by 여성게
:
Web/JPA 2019. 2. 11. 23:43

JPA - JPAQuery와 Q클래스


긴 포스팅은 아니지만 예제로 소스를 짜는 도중에 몰랐던 내용을 알게되어서 작성한다. QueryDSL로 예제중 나온 상황이다.



1
2
3
4
5
6
7
8
9
10
public List<MyEntity> findMyDumbEntities(long id) {    
      QMyEntity qMyEntity = QMyEntity.myEntity;
      JPAQuery jpaQuery = new JPAQuery(entityManager);
      MyEntity myFirstEntity = jpaQuery.from(qMyEntity)
                                        .where(qMyEntity.id.eq(id)).uniqueResult(qMyEntity);
      MyEntity mySecondEntity = jpaQuery.from(qMyEntity)
                                        .where(qMyEntity.id.eq(id+1)).uniqueResult(qMyEntity);
 
      return Arrays.asList(myFirstEntity, mySecondEntity);    
}
cs


언뜻 보기에는 크게 문제가 없는 듯 싶다. 하지만 이 소스를 실행하면 이러한 예외가 발생한다.



java.lang.IllegalStateException: qMyEntity is already used


즉, 같은 JPAQuery 객체로 같은 Q타입클래스를 사용할 수없는 것이다. 사실 내부를 까보지는 않아서 원인파악은 해보아야겠지만 일단 결론은 그렇다. 그렇기 때문에 소스가 이런식으로 변경되어야한다.


1
2
3
4
5
6
7
8
9
public List<MyEntity> findMyDumbEntities(long id) {        
          QMyEntity qMyEntity = QMyEntity.myEntity;
          MyEntity myFirstEntity = new JPAQuery(entityManager).from(qMyEntity)
                                    .where(qMyEntity.id.eq(id)).uniqueResult(qMyEntity);
          MyEntity mySecondEntity = new JPAQuery(entityManager).from(qMyEntity)
                                    .where(qMyEntity.id.eq(id+1)).uniqueResult(qMyEntity);
 
          return Arrays.asList(myFirstEntity, mySecondEntity);        
}
cs


이런식으로 JPAQuery 객체를 만드는 메소드를 하나 정의하는 것이 좋을 것같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
 
protected JPQLQuery jpaQuery() {
    return new JPAQuery(entityManager);
}
 
@Override
public List<Article> findAll() {
      return jpaQuery().from(article).fetchAll().list(article);
}
 
public Article findById(@Nonnull final long id) {
      return jpaQuery().from(article).where(article.id.eq(id)).uniqueResult(article);
}
cs

언젠가는 이런 상황이 닥칠수도 있을 것같아서 포스팅한다..

posted by 여성게
:
Web/JPA 2019. 2. 11. 22:43

JPA - NativeQuery ( SQL ) 네이티브 SQL 사용하기!


JPA는 SQL이 지원하는 대부분의 문법과 SQL 함수들을 지원하지만 특정 데이터베이스에 종속적인 기능은 잘 지원하지 않는다. 하지만 때론 특정 데이터베이스에 종속적인 기능이 필요할 수도 있다. 다양한 이유로 JPQL을 사용할 수 없을 때, JPA는 SQL을 직접 사용할 수 있는 기능을 제공하는데 이것을 네이티브 SQL(네이티브쿼리)라고 한다. 즉, 사용자가 직접 데이터베이스에 날리는 쿼리를 작성하는 것이다. 그렇다면 JPA가 지원하는 네이티브 SQL과 JDBC API를 직접 사용하는 것에는 어떤 차이가 있냐? 그것은 바로 네이트브 쿼리는 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다는 것이다!



Native Query API



  1. //결과 타입 정의
  2. public Query createNativeQuery(String sqlString, Class resultClass) {}
  3. //결과 타입을 정의할 수 없을 때
  4. public Query createNativeQuery(String sqlString) {}
  5. //엔티티 타입이 아닌 결과 값일때
  6. public Query createNativeQuery(String sqlString
  7.                         ,String resultSetMapping) {}

 이렇게 세가지의 오버로딩된 메소드를 사용한다.




  1. /*
  2.  * 네이티브쿼리는 위치기반 파라미터 바인딩만 지원한다.(구현체중 일부는 이름기반 파라미터 바인딩도 지원함)
  3.  */
  4. public void nativeQuery() {
  5.         String sql = "SELECT MEMBER_JPQL_ID,USERAGE,USERNAME,TEAM_ID FROM MEMBER_JPQL WHERE USERAGE> ?";
  6.         
  7.         Query nativeQuery = em.createNativeQuery(sql,MemberJPQL.class).setParameter(135);
  8.         
  9.         List<MemberJPQL> resultList = nativeQuery.getResultList();
  10.         
  11.         System.out.println("====================nativeQuery=====================");
  12.         
  13.         for(MemberJPQL m : resultList) {
  14.                 System.out.println(m.toString());
  15.         }
  16.         
  17.         System.out.println("====================nativeQuery=====================");
  18. }

리턴타입이 엔티티일 경우이다. em.createNativeQuery(SQL문장,리턴타입클래스)로 Query객체를 만들어낸다. 위의 예제를 보면 JPQL과 다를 것은 쿼리를 직접작성하냐 안하냐의 차이지 대부분의 로직은 비슷하다.(JPA가 자동으로 생성해주는 SQL를 사용자가 직접 작성한 것뿐 더이상의 차이는 거의없다.)

그리고 Native Query에서는 반환타입이 명시되어도 Query 객체로 반환받는다는 점을 유의하자.





다음은 결과 매핑 사용 예제이다.


  1. @Table(name = "MEMBER_JPQL")
  2. //@Getter
  3. //@Setter
  4. @ToString
  5. @SqlResultSetMapping(name = "memberWithOrderCount",
  6.                                         entities = {@EntityResult(entityClass = MemberJPQL.class)},
  7.                                         columns = {@ColumnResult(name = "ORDER_COUNT")})
  8. public class MemberJPQL {....}


  1. /*
  2.  * ResultMapping
  3.  */
  4. public void nativeQuery2() {
  5.         String sql = "SELECT M.MEMBER_JPQL_ID AS ID,USERAGE AS AGE,USERNAME,TEAM_ID,I.ORDER_COUNT "
  6.                                 + "FROM MEMBER_JPQL M JOIN  "
  7.                                                 + "(SELECT IM.MEMBER_JPQL_ID, COUNT(*) AS ORDER_COUNT "
  8.                                                 + "FROM ORDER_JPQL O, MEMBER_JPQL IM "
  9.                                                 + "WHERE O.MEMBER_JPQL_ID = IM.MEMBER_JPQL_ID) I "
  10.                                 + "ON M.MEMBER_JPQL_ID = I.MEMBER_JPQL_ID";
  11.                                 
  12.         Query nativeQuery = em.createNativeQuery(sql,"memberWithOrderCount");
  13.         
  14.         List<Object[]> resultList = nativeQuery.getResultList();
  15.         
  16.         System.out.println("====================nativeQuery2=====================");
  17.         
  18.         for(Object[] m : resultList) {
  19.                 System.out.println(m[0].toString());
  20.                 System.out.println(m[1]);
  21.         }
  22.         
  23.         System.out.println("====================nativeQuery2=====================");
  24. }

특별한 것은 없다. 엔티티클래스에 네이티브 쿼리로 받을 반환 타입을 명시해주었고, 네이티브 쿼리를 사용한 쪽에서는 반환타입을 엔티티클래스에 작성한 반환타입 이름 값을 문자열로 넘겨주었다. 그래서 반환된 Object[]에서 Object[0]은 회원엔티티로 매핑되고, Object[1]은 스칼라타입으로 ORDER_COUNT를 담고있다.

어노테이션을 보면 속성 값이 entities,columns로 된것으로 보아 여러개의 엔티티,칼럼을 매핑할 수 있다. 그리고 @SqlResultSetMappings 어노테이션이 있기 때문에 여러개의 매핑타입을 만들 수 있다.



그런데 대부분 엔티티 클래스의 id클래스의 필드명은 "id"로 거의 동일하게 하는 경우도 많다. 그래서 조회를 하면 칼럼명이 중복나는 경우가 생기게 된다. 그때는 필드매핑까지 포함하여서 조회가능하다.


  1. @Table(name = "MEMBER_JPQL")
  2. //@Getter
  3. //@Setter
  4. @ToString
  5. @SqlResultSetMappings({
  6.         @SqlResultSetMapping(name = "memberWithOrderCount",
  7.                         entities = {@EntityResult(entityClass = MemberJPQL.class)},
  8.                         columns = {@ColumnResult(name = "ORDER_COUNT")}
  9.         )
  10.         ,@SqlResultSetMapping(name = "memberInfo",
  11.                         entities = {
  12.                                         @EntityResult(entityClass=MemberJPQL.class,
  13.                                         fields = {
  14.                                                         @FieldResult(name = "id",column = "member_id")
  15.                                                         ,@FieldResult(name = "age",column = "client_age")
  16.                                         })
  17.                         }
  18.         )
  19.         
  20. })
  21. public class MemberJPQL {...}
  1. public void nativeQuery3() {
  2.         String sql = "select MEMBER_JPQL_ID as member_id,USERAGE as client_age "
  3.                         + "from member_jpql where USERNAME = '여성게1'";
  4.         
  5.         Query nativeQuery = em.createNativeQuery(sql,"memberInfo");
  6.         
  7.         List<MemberJPQL> resultList = nativeQuery.getResultList();
  8.         
  9.         System.out.println("====================nativeQuery3=====================");
  10.         
  11.         for(MemberJPQL m : resultList) {
  12.                 System.out.println(m.toString());
  13.         }
  14.         
  15.         System.out.println("====================nativeQuery3=====================");
  16. }

부득이하게 조회할 컬럼명을 별칭으로 주었을 경우, 엔티티 클래스와 매핑이 되지 않는다. 그럴때 엔티티클래스에 필드매핑을 이용한다.


@FieldResult(name = "id",column = "member_id")


name이 엔티티클래스의 필드명이고, column이 조회하였을 때 반환되는 컬럼명이다.



마지막으로 이전에 다루었던 정적쿼리(@NamedQuery)처럼 네이티브 쿼리로 정적쿼리처럼 엔티티클래스에 정의할 수 있다.




▶︎▶︎▶︎JPA - @NamedQuery , 정적 쿼리



    1. @Table(name = "MEMBER_JPQL")
    2. //@Getter
    3. //@Setter
    4. @ToString
    5. @SqlResultSetMappings({
    6.         @SqlResultSetMapping(name = "memberWithOrderCount",
    7.                         entities = {@EntityResult(entityClass = MemberJPQL.class)},
    8.                         columns = {@ColumnResult(name = "ORDER_COUNT")}
    9.         )
    10.         ,@SqlResultSetMapping(name = "memberInfo",
    11.                         entities = {
    12.                                         @EntityResult(entityClass=MemberJPQL.class,
    13.                                         fields = {
    14.                                                         @FieldResult(name = "id",column = "member_id")
    15.                                                         ,@FieldResult(name = "age",column = "client_age")
    16.                                         })
    17.                         }
    18.         )
    19.         
    20. })
    21. @NamedNativeQueries({
    22.         @NamedNativeQuery(name = "Member.memberSQL",
    23.                                                 query = "select member_jpql_id,userage,username,team_id "
    24.                                                                 + "from member_jpql "
    25.                                                                 + "where username = ?",
    26.                                                 resultClass=MemberJPQL.class)
    27.         ,@NamedNativeQuery(name = "Member.memberSQL2",
    28.                         query = "select MEMBER_JPQL_ID as member_id,USERAGE as client_age "
    29.                                         + "from member_jpql where USERNAME = ?",
    30.                         resultSetMapping = "memberInfo")
    31. })
    32. public class MemberJPQL {}


    1. public void nativeQuery4() {
    2.         
    3.         Query nativeQuery = em.createNamedQuery("Member.memberSQL",MemberJPQL.class)
    4.                         .setParameter(1"여성게1");
    5.         
    6.         List<MemberJPQL> resultList = nativeQuery.getResultList();
    7.         
    8.         System.out.println("====================nativeQuery4=====================");
    9.         
    10.         for(MemberJPQL m : resultList) {
    11.                 System.out.println(m.toString());
    12.         }
    13.         
    14.         System.out.println("====================nativeQuery4=====================");
    15. }
    16. public void nativeQuery5() {
    17.         
    18.         Query nativeQuery = em.createNamedQuery("Member.memberSQL2")
    19.                         .setParameter(1"여성게1");
    20.         
    21.         List<MemberJPQL> resultList = nativeQuery.getResultList();
    22.         
    23.         System.out.println("====================nativeQuery5=====================");
    24.         
    25.         for(MemberJPQL m : resultList) {
    26.                 System.out.println(m.toString());
    27.         }
    28.         
    29.         System.out.println("====================nativeQuery5=====================");
    30. }


    em.createNativeQuery()에서 다루던 것을 어노테이션으로 다 가져다 놓았다고 보면된다. 그리고 조금 특이한 것은 @NamedQuery와 같이 em.createNamedQuery()를 사용한다는 것이다.

    posted by 여성게
    :
    Web/JPA 2019. 2. 11. 01:07

    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을 참고해보시고 더 편하신 것을 사용하는 것이 좋을것 같습니다.


    posted by 여성게
    :
    Web/JPA 2019. 2. 9. 18:59


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




    Criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API이다. Criteria를 사용하면 문자가 아닌 코드로 JPQL를 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고 문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있다. 하지만 이번에 Criteria를 간단하게만 다루려고 한다. 이유는 실제 Criteria를 사용해서 개발하다보면 코드가 복잡하고 장황하며 직관적으로 이해하기 힘들다는 단점이 있기때문이다. 다음 포스팅에서는 코드로 쿼리를 작성할 수 있고 간단한 QueryDSL을 다음에 자세히 다뤄볼 예정이기에 이번 포스팅은 몇가지 간단하게만 다뤄본다.





    Criteria 기초



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
    CriteriaBuilder builder = em.getCriteriaBuilder(); --> 1
    //CriteriaBuilder로부터 CriteriaQuery객체를 얻어온다. 
    //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
    CriteriaQuery<MemberJPQL> query = builder.createQuery(MemberJPQL.class); --> 2
            
    //조회의 시작점을 뜻하는 Root객체 생성 여기서 변수명 m은 JPQL에서 별칭이라고 생각하면 된다.
    //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
    Root<MemberJPQL> m = query.from(MemberJPQL.class); --> 3
    //distinct 중복제거.
    query.select(m).distinct(true); --> 4
        
    //위에서 반환타입이 명확하지 않다면 Query 객체로 쿼리를 만들고 결과를 받는다.
    TypedQuery<MemberJPQL> typeQuery = em.createQuery(query);
        
    List<MemberJPQL> members = typeQuery.getResultList();
    cs

       



        1  

     Criteria 쿼리를 생성하려면 먼저 Criteria 빌더(CriteriaBuilder)를 얻어야 한다. Criteria 빌더는 EntityManager나 EntityManagerFactory에서 얻을 수 있다. 

        2

     Criteria 쿼리 빌더에서  Criteria 쿼리( CriteriaQuery)를 생성한다. 이때 반환 타입을 지정할 수 있다.

        3

     FROM 절을 생성한다. 반환된 값 m은  Criteria에서 사용하는 특별한 별칭이다. m을 조회의 시작점이라는 의미로 쿼리 루트(Root)라고 한다.

        4

     SELECT 절을 생성한다. 


    이렇게  Criteria쿼리를 완성하고 나면 다음 순서는 기존의 JPQL과 같다. em.createQuery(query)에 완성된  Criteria쿼리를 넣어주면 된다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
    CriteriaBuilder builder = em.getCriteriaBuilder();
        
    //CriteriaBuilder로부터 CriteriaQuery객체를 얻어온다. 
    //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
    CriteriaQuery<MemberJPQL> query = builder.createQuery(MemberJPQL.class);
     
    //조회의 시작점을 뜻하는 Root객체 생성 여기서 변수명 m은 JPQL에서 별칭이라고 생각하면 된다.
    //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
    Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
    //검색조건 정의
    Predicate usernameEqual = builder.equal(m.get("username"), "여성게1");
    //정렬조건 정의
    Order ageDesc = builder.desc(m.get("age"));
     
    //distinct 중복제거.
    query.select(m)
         .distinct(true)
         .where(usernameEqual)
         .orderBy(ageDesc);
     
    cs

      


    다음으로는 검색조건,정렬조건을 정의하는 부분이다. 위와 같이 where절에 들어가는 부분은 Predicate 클래스로, orderBy에 들어가는 부분은 Order 클래스로 정의해준다. 위와 같이 객체로 정의하여도 되지만


    ~.where(builder.equal(m.get("username"),"여성게1"); 

    -->이런식으로 where절에 바로 넣어주어도 된다.






    조회(한건,여러건) - 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
    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
    public void select() {
     
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
        //CriteriaBuilder로부터 CriteriaQuery객체를 얻어온다. 
        //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
        CriteriaQuery<MemberJPQL> query = builder.createQuery(MemberJPQL.class);
        
        //조회의 시작점을 뜻하는 Root객체 생성 여기서 변수명 m은 JPQL에서 별칭이라고 생각하면 된다.
        //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
        //검색조건 정의
        Predicate usernameEqual = builder.equal(m.get("username"), "여성게1");
        //정렬조건 정의
        Order ageDesc = builder.desc(m.get("age"));
     
        //distinct 중복제거.
        query.select(m)
             .distinct(true)
             .where(usernameEqual)
             .orderBy(ageDesc);
     
        //위에서 반환타입이 명확하지 않다면 Query 객체로 쿼리를 만들고 결과를 받는다.
        TypedQuery<MemberJPQL> typeQuery = em.createQuery(query);
     
        List<MemberJPQL> members = typeQuery.getResultList();
        
        System.out.println("=================select==================");
     
        for(MemberJPQL member : members) {
            System.out.println(member.toString());
        }
     
        System.out.println("=================select==================");
    }
     
        
    /*
     * 조회대상을 여러건 지정할때
     * ex) select m.name,m.age from MemberJPQL m
     */
     
    public void select2() {
     
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
        //CriteriaBuilder로부터 CriteriaQuery객체를 얻어온다. 
        //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
        CriteriaQuery<Object[]> query = builder.createQuery(Object[].class);
     
        //조회의 시작점을 뜻하는 Root객체 생성 여기서 변수명 m은 JPQL에서 별칭이라고 생각하면 된다.
        //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
        query.multiselect(m.get("username"),m.get("age")).distinct(true);
     
        //query.select(builder.array(m.get("username"),m.get("age"))).distinct(true); ->위와 같다.
     
        TypedQuery<Object[]> query2 = em.createQuery(query);
        List<Object[]> resultList = query2.getResultList();
     
        System.out.println("=================select2==================");
     
        for(Object o : resultList) {
            Object[] o2 = (Object[])o;
            System.out.print(o2[0]+" ");
            System.out.println(o2[1]);
        }
     
        System.out.println("=================select2==================");
    }
    cs



    크게 어려울 부분은 없을 것같다. select()는 단일조회, select2()는 다중값 조회이다. 하지만 여기서 JPQL을 생각해보면 여러 값의 조회를 하나의 DTO클래스로 반환받았던 것이 기억날 것이다. Criteria 역시 NEW 명령어 쿼리를 지원한다.




    NEW 명령어를 이용하여 다중값 특정 객체 매핑이후 반환 - 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
    29
    /*
     * 반환타입을 알수 없을때, 여러개의 반환타입을 가질 경우 DTO클래스로 반환값을 갖는다.
     */
    public void constructSelect() {
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
        //CriteriaBuilder로부터 CriteriaQuery객체를 얻어온다. 
        //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
        CriteriaQuery<MemberDTO> query = builder.createQuery(MemberDTO.class);
     
        //조회의 시작점을 뜻하는 Root객체 생성 여기서 변수명 m은 JPQL에서 별칭이라고 생각하면 된다.
        //반환타입을 알수 없다면 제네릭타입을 Object로 준다.
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
        query.select(builder.construct(MemberDTO.class, m.get("username"),m.get("age")) );
     
        TypedQuery<MemberDTO> typeQuery = em.createQuery(query);
            
        List<MemberDTO> members = typeQuery.getResultList();
     
        System.out.println("=================constructSelect==================");
     
        for(MemberDTO member : members) {
            System.out.println(member.toString());
        }
        
        System.out.println("=================constructSelect==================");
    }
    cs



    JPQL과 비교하여 달라진 점이 좀있다. 문자기반이 아니라 코드기반으로 쿼리를 다루다보니 DTO클래스를 풀 패키지명으로 안써도 되는 것이다. 이런것들을 생각하면 문자열을 잘못넣어서 오류날 일은 거의 없을듯 싶다. 하지만 아직까지도 문자열을 쓰는 부분은 조금 남아있다.




    Java Map과 비슷한 TupleQuery - Criteria



    만약 반환값이 여러개라면 위처럼 DTO로 받을 수도 있지만 마치 Map과 같은 객체처럼 느껴지는 Tuple이라는 객체를 이용해서도 다중 값 쿼리가 가능하다.


    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
    /*
     * 튜플 - Java의 Map과 흡사한 형태로 결과를 반환받는다.
     */
    public void tupleQuery() {
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
        CriteriaQuery<Tuple> query = builder.createTupleQuery();
     
        //CriteriaQuery<Tuple> query = builder.createQuery(Tuple.class); -> 위와 같다.
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
        query.multiselect(m.get("username").alias("username"),m.get("age").alias("age"));
        //query.select(builder.tuple(m.get("username").alias("username"),m.get("age").alias("age")))
     
        ->위와같다.
     
        /*
         * 멤버전체를 하나의 튜플로 받을때.
         * 
         * query.select(builder.tuple(m.alias("m"))); 
         * 이러면 m이라는 별칭으로 tuple에서 회원엔티티를 얻어올 수 있다.
         */
        TypedQuery<Tuple> query2 = em.createQuery(query);
        List<Tuple> resultList = query2.getResultList();
     
        System.out.println("=================tupleQuery==================");
     
        for(Tuple t : resultList) {
            System.out.println(t.get("username",String.class)+" "+t.get("age",Integer.class));
        }
        System.out.println("=================tupleQuery==================");
    }
    cs



    코드만 봐도 조금은 직관적으로 보인다. Tuple의 key가 될 값을 반환value.alias(key값)로 지정해준다. 그런다음 결과를 꺼낼때 Tuple객체에서 키값과 반환타입을 넘겨 꺼낸다. 위의 주석에도 있듯이 스칼라타입의 필드말고 엔티티를 전체로 튜플의 값으로도 설정해줄수 있다.

    결론적으로 튜플은 이름 기반이므로 순서 기반의 Object[]로 반환받는 것보다 훨씬 안전하다. 그리고 tuple.getElements() 같은 메소드를 사용해서 현재 튜플의 별칭과 자바 타입도 조회가능하다.






    조인(Join & Fetch Join) - 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
    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
    /*
     * Join
     * 
     * public enum JoinType{
     *     INNER,LEFT,RIGHT
     * }
     */
    public void join() {
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
        CriteriaQuery<Tuple> query = builder.createTupleQuery();
     
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
        //JPQL에서 조인구문과 연관 엔티티의 별칭이라 보면된다.
        Join<MemberJPQL, TeamJPQL> t =m.join("team",JoinType.INNER);
     
        query.multiselect(m.alias("m"),t.alias("t"))
             .where(builder.equal(t.get("name"), "티스토리1"));
     
        TypedQuery<Tuple> query2 = em.createQuery(query);
        List<Tuple> resultList = query2.getResultList();
     
        System.out.println("=================join==================");
     
        for(Tuple tuple : resultList) {
            MemberJPQL member = tuple.get("m",MemberJPQL.class);
            TeamJPQL team = tuple.get("t",TeamJPQL.class);
            System.out.println(member.toString());
            System.out.println(team.toString());
        }
     
        System.out.println("=================join==================");
    }
     
    public void fetchJoin() {
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
        CriteriaQuery<MemberJPQL> query = builder.createQuery(MemberJPQL.class);
     
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
        //fetchJoin이다. 만약 외부조인이라면 밑에 주석과 같이 left,right를 넣어준다.
        m.fetch("team"/*,JoinType.LEFT*/);
     
        query.select(m)
             .where(builder.equal(m.get("username"), "여성게1"));
        
        TypedQuery<MemberJPQL> query2 = em.createQuery(query);
        List<MemberJPQL> resultList = query2.getResultList();
     
        System.out.println("=================fetchJoin==================");
     
        for(MemberJPQL member : resultList) {
            System.out.println(member.toString());
        }
     
        System.out.println("=================fetchJoin==================");
    }
    cs



    JPQL에서 다루었던 조인과 패치조인이다. 크게 어려울 부분은 없을 것같아 몇가지만 설명하려고 한다. 


    Join<MemberJPQL, TeamJPQL> t =m.join("team",JoinType.INNER);

    ->JPQL과 비교하면 ~ [inner] join m.team t 와 비슷한 구문이라고 보면된다.


    그리고 튜플로 반환받기위해 두개의 엔티티 모두에게 별칭을 주었고 마지막 결과를 출력하는 부분에 별칭을 이용하여 객체를 얻어왔다.



    그 다음은 패치조인이다.


    m.fetch("team"/*,JoinType.LEFT*/);

    -> 

    일반 조인과는 다르게 반환값을 받아서 사용하지 않는다. 즉, 조회의 결과로 나오는 회원엔티티의 team필드에 값이 세팅되서 나오는 것이다. 그래서 결과값을 받을 경우 회원엔티티로만 받고 그 회원엔티티의 팀엔티티 필드에서 값을 꺼내 사용한다.(사실 Criteria에서 패치조인을 이용하여 두개의 결과값을 받을 수 있는지는 확실히 모르겠다. 혹시 아시는 분은 댓글부탁드립니다.)





    Parameter Binding



    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
    public void paramBind(String param) {
     
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
     
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
            
     
        CriteriaQuery<MemberJPQL> query = builder.createQuery(MemberJPQL.class);
     
            
     
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
            
     
        //fetchJoin이다. 만약 외부조인이라면 밑에 주석과 같이 left,right를 넣어준다.
     
        m.fetch("team"/*,JoinType.LEFT*/);
     
            
     
        query.select(m).where(builder.equal(m.get("username")
     
                     ,builder.parameter(String.class,"username")));
     
        TypedQuery<MemberJPQL> query2 = em.createQuery(query)
     
                        .setParameter("username", param);
     
            
     
        List<MemberJPQL> resultList = query2.getResultList();
     
            
     
        System.out.println("=================paramBind==================");
     
            
     
        for(MemberJPQL member : resultList) {
     
            System.out.println(member.toString());
     
        }
     
            
     
        System.out.println("=================paramBind==================");
     
    }
    cs


    query.select(m).where(builder.equal(m.get("username")

     ,builder.parameter(String.class,"username")));

    -> 이름기반 파라미터 정의를 해준다. (Predicate 객체로 선언해도되지만 이렇게 where절에 바로 넣어도된다.)




    TypedQuery<MemberJPQL> query2 = em.createQuery(query)

    .setParameter("username"param);

    ->JPQL과 동일하게 setParameter메소드를 호출해서 세팅해준다.



    Criteria Meta Code Generate



    지금까지 간단하게 Criteria 쿼리를 설명했다. 확실히 그냥 JPQL을 사용할때 보다 문자열을 사용하여 쿼리를 작성하는 부분이 확 줄었다. 그렇지만 그렇다고 100% 문자열을 사용하지 않는 것은 아니다. 그 말은 100% 실수하지 않으리라는 보장이 없는 것이다. 이때 사용할 수 있는 것이 Meta Code를 생성하여서 위에서 조금 사용한 문자열마저 사용하지 않는 것이다. Meta Code를 생성하면 각 엔티티 클래스마다 메타정보를 가진 자바파일들이 생성되게 된다.



    <pom.xml>


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- Criteria Meta Code -->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-jpamodelgen</artifactId>
        <version>1.3.0.Final</version>
    </dependency>
     
    <!-- Criteria Meta Code Plugin -->
     
    <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <configuration>                    
            <source>1.8</source>
            <target>1.8</target>
            <outputDirectory>target/generated-sources/annotations</outputDirectory>
            <compilerArguments>
                <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
             </compilerArguments>
        </configuration>
    </plugin>
    cs



    이렇게 두개를 설정해준다. 첫번째 의존 라이브러리를 내려받아주고, 두번째는 Meta Code를 생성하기 위한 플러그인 설정을 해주는 것이다.





    ->Run As..->Run Configuration 에 들어가서 해당 프로젝트의 메이븐 빌드 Goals를 입력하고 Run을 해준다.




    ->빌드가 완료되었다면 target->generated-sources에 Meta Code들이 생성되어 있을 것이다. 이 다음은 과정은 각자의 IDE환경에 따라 다르다. 만약 target>generated-sources가 자동으로 클래스패스로 잡힌다면 문제가 없지만 만약 클래스패스로 잡히지 않는다면 직접 등록해주어야한다. 클래스패스가 디폴트로 잡히지 않는다는 가정하에 진행한다.








    -> 첫 이미지처럼 반드시 패키지명직전까지를 패스로 잡아야한다. 그래서 Meta Code생성시 package관련 에러가 뜨지 않는다. 마지막으로 Apply를 누르면 방금생성된 메타코드가 클래스패스에 들어가있을 것이다(기존의 target부분에는 코드가 없어짐). 혹시나 에러표시가 프로젝트에 계속 난다면 maven update를 하길바란다.






    Criteria Meta Code 적용후 쿼리




    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
    /*
     * Criteria Meta Code 생성
     */
    public void afterMetaCodeGen(String username,String teamname) {
        //EntityManager 혹은 EntityManagerFactory에서 CriteriaBuilder를 가져온다.
        CriteriaBuilder builder = em.getCriteriaBuilder();
     
        CriteriaQuery<MemberJPQL> query = builder.createQuery(MemberJPQL.class);
        Root<MemberJPQL> m = query.from(MemberJPQL.class);
     
        //fetchJoin이다. 만약 외부조인이라면 밑에 주석과 같이 left,right를 넣어준다.
        m.fetch("team"/*,JoinType.LEFT*/);
     
        List<Predicate> searchConditions = new ArrayList<Predicate>();
     
        if(!StringUtils.isEmpty(username)){
            searchConditions.add(builder.equal(m.get(MemberJPQL_.username)
                                ,builder.parameter(String.class,"username")));
        }
     
        if(!StringUtils.isEmpty(teamname)) {
            searchConditions.add(builder.equal(m.get(MemberJPQL_.team).get(TeamJPQL_.name)
                    ,builder.parameter(String.class,"teamname")));
        }
     
        query.select(m)
             .where(builder.and(searchConditions.toArray(new Predicate[0])));
     
        TypedQuery<MemberJPQL> query2 = em.createQuery(query);
     
        if(!StringUtils.isEmpty(username)){
            query2.setParameter("username", username);
        }
     
        if(!StringUtils.isEmpty(teamname)) {
            query2.setParameter("teamname", teamname);
        }
     
        List<MemberJPQL> resultList = query2.getResultList();
     
        System.out.println("=================afterMetaCodeGen==================");
     
        for(MemberJPQL member : resultList) {
            System.out.println(member.toString());
        }
     
        System.out.println("=================afterMetaCodeGen==================");
     
    }
    cs




    m.get(MemberJPQL_.username)

    ->여기에서 사용하던 문자열이 없어졌다. 이제 조금은 에러가 줄것 같다.






    지금까지 간단하게 Criteria를 다뤄봤습니다. 사실 개인적인 생각으로는 이것이 편해진것인가...? 하는 생각이 조금 들었습니다. 문자기반으로 작성하지 않을 뿐이고 오히려 코드는 그냥 JPQL을 쓸때보다 복잡해진것같기도 하는 생각이 듭니다.(개인적인 생각입니다. 물론 런타임시점에 에러를 발견해야하는 큰 문제를 해결해준 것만으로도 엄청난 가치가 있다고 생각듭니다.) 그래서 다음 포스팅에서는 쉽고, 간단하고, 코드로 쿼리를 다룰 수 있는 QueryDSL을 이용하여 JPA쿼리를 다뤄보겠습니다.



    posted by 여성게
    :
    Web/JPA 2019. 2. 9. 14:49

    JPA - JPQL 서브쿼리 (객체지향 쿼리 언어),컬렉션 식


    JPQL도 서브 쿼리를 지원하지만 몇 가지 제약사항이 존재한다.


    -WHERE, HAVING 절에서만 사용할 수 있고, SELECT,FROM절에서는 사용할 수 없다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /*
     * 서브쿼리 - SQL과 다르게 where,having 절에서만 사용가능하다.
     */
    public void subQuery() {
        String jpql = "select m from MemberJPQL m where m.age > (select avg(m2.age) from MemberJPQL m2)";
        TypedQuery<MemberJPQL> query = em.createQuery(jpql,MemberJPQL.class);
        List<MemberJPQL> teams = query.getResultList();
     
        System.out.println("================subQuery=================");
     
        for(MemberJPQL m : teams) {
            System.out.println(m.toString());
        }
        System.out.println("================subQuery=================");
    }
    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
    /*
     * 서브쿼리 함수
     * [NOT] EXISTS (subquery) - subquery에 결과가 존재하면 참이다. NOT은 반대
     * {ALL | ANY | SOME } (subquery) - ALL 서브쿼리의 모든결과가 참이면 참, ANY 서브쿼리의 결과중 아무거나 참이면 참, 
    SOME == ANY(반드시 비교연산자와 사용)
     * [NOT] IN (subquery) - 서브쿼리 결과 중 하나라도 같은 것이 있으면 참이다.
     *                        - 서브쿼리가 아닌 곳에서도 사용가능 ~ IN ('여성게1','여성게2')
     */
    public void subQueryFunction() {
        String jpql = "select m from MemberJPQL m where m.age < ALL (select avg(m2.age)"
                    +"from MemberJPQL m2)";
     
        TypedQuery<MemberJPQL> query = em.createQuery(jpql,MemberJPQL.class);
        List<MemberJPQL> teams = query.getResultList();
     
        System.out.println("================subQueryFunction=================");
     
        for(MemberJPQL m : teams) {
            System.out.println(m.toString());
        }
     
        System.out.println("================subQueryFunction=================");
     
    }
    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
    /*
     * 연관관계 컬렉션에 사용할 수 있는 수식? 컬렉션 식이다.
     * is [not] empty - 연관관계에 있는 컬렉션이 비어있는가?
     * {엔티티나 값} [not] member of {컬렉션 값 연관경로} - 엔티티나 값이 컬렉션에 포함되어있는가?
     */
    public void collectionFunction() {
        /*String jpql = "select t from TeamJPQL t where t.members is not empty and t.name ='티스토리1'";*/
        String jpql = "select t from TeamJPQL t where :memberParam member of t.members";
        TypedQuery<MemberJPQL> query2 = em.createQuery("select m from MemberJPQL m where m.username='여성게1'",MemberJPQL.class);
        MemberJPQL member = query2.getResultList().get(0);
     
        TypedQuery<TeamJPQL> query = em.createQuery(jpql,TeamJPQL.class);
        query.setParameter("memberParam", member);
        List<TeamJPQL> teams = query.getResultList();
     
        System.out.println("================collectionFunction=================");
     
        for(TeamJPQL m : teams) {
            System.out.println(m.toString());
        }
     
        System.out.println("================collectionFunction=================");
     
    }
    cs






    posted by 여성게
    :
    Web/JPA 2019. 2. 9. 14:41

    JPA - JPQL 조인(객체지향쿼리),Java Persistence Query Language 



    JPQL 조인은 SQL 조인과 기능은 거의 같고 문법만 약간 다르다.



    내부 조인(inner join)



    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
    /*
     * 내부조인
     */
    public void innerJoin() {
        String jpql = "select m,t "
                + "from MemberJPQL m inner join m.team t "
                + "where t.name = '티스토리1' "
                ;
     
        Query query = em.createQuery(jpql);
        List members = query.getResultList();
     
        System.out.println("================innerJoin=================");
     
        for(Object o : members) {
            Object[] result = (Object[]) o;
            for(Object o2 : result) {
                System.out.print("result element => "+o2);
            }
            System.out.println();
        }
     
        System.out.println("================innerJoin=================");
     
    }
    cs



    JPQL 내부 조인 구문을 보면 SQL 문법과는 약간 다르다. SQL에서는 조인문에 연관테이블의 외래키로 직접 조인을 하지만 JPQL에서는 from 절의 엔티티의 연관필드로 조인하게 되어 있다. 만약 SQL처럼 조인하면 안된다. (ex ~ FROM MemberJPQL m JOIN TeamJPQL t --->오류발생)


    그리고 select 다음에 만약 조인한 엔티티의 특정필드만 뽑는다면 위와 같이 꼭 조인할 연관엔티티에 별칭을 넣어줘야한다.





    외부조인(outer join)



    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 outerJoin() {
        String jpql = "select m,t "
                + "from MemberJPQL m left join m.team t "
                + "where t.name = '티스토리1' "
                ;
     
        Query query = em.createQuery(jpql);
        List members = query.getResultList();
     
        System.out.println("================outerJoin=================");
     
        for(Object o : members) {
            Object[] result = (Object[]) o;
            for(Object o2 : result) {
                System.out.print("result element => "+o2);
            }
     
            System.out.println();
        }
     
        System.out.println("================outerJoin=================");
     
    }
    cs




    컬렉션 조인(Collection Join)



    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
    /*
     * 컬렉션조인 - 일대다,다대다 관계처럼 필드에 컬렉션을 사용하는 곳에 조인하는 것.
     * 
     * ex) Team - > Member 일대다관계
     * 즉, 컬렉션을 연관필드로 조인하는 것이다.
     */
    public void collectionJoin() {
        String jpql = "select t,m "
                + "from TeamJPQL t join t.members m "
                + "where t.name = '티스토리1' "
                ;
     
        Query query = em.createQuery(jpql);
        List members = query.getResultList();
     
        System.out.println("================collectionJoin=================");
     
        for(Object o : members) {
            Object[] result = (Object[]) o;
            for(Object o2 : result) {
                System.out.print("result element => "+o2);
            }
     
            System.out.println();
        }
     
        System.out.println("================collectionJoin=================");
     
    }
    cs



    위 주석에도 달려있지만 일대다, 다대다 관계처럼 연관필드에 컬렉션으로 정의되어있는 엔티티를 조인하는 방법이다.




    세타조인(Theta Join)



    WHERE 절을 사용해서 세타조인을 할 수 있다. SQL에서도 그렇듯이 세타 조인은 내부 조인만 가능하다. 그리고 위에서 했던 조인들은 연관된 엔티티필드를 이용해 조인을 했지만, 세타 조인은 전혀 관계없는 엔티티도 조인할 수 있다. 여기서 전혀 관계가 없다는 것은 매핑된 외래키로 조인하는 것이 아닌 일반 필드로 조인이 가능하다는 뜻이다.



    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
    /*
     * 세타조인 - where절을 이용한 조인
     *           - 내부조인만 지원하며, 연관필드를 가지지 않는 전혀 관계없는 엔티티도 조인해 결과로 리턴할 수 있다.
     */
    public void thetaJoin() {
        String jpql = "select t,m "
                + "from TeamJPQL t ,MemberJPQL m "
                + "where m.team.name = t.name and t.name = '티스토리1'"
                ;
     
        Query query = em.createQuery(jpql);
        List members = query.getResultList();
     
        System.out.println("================thetaJoin=================");
     
        for(Object o : members) {
            Object[] result = (Object[]) o;
            for(Object o2 : result) {
                System.out.print("result element => "+o2);
            }
            System.out.println();
        }
     
        System.out.println("================thetaJoin=================");
     
    }
    cs





    Join on 절



    JPA 2.1 버전부터 지원하는 기능이며, On절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 참고로 내부조인의 On절은 where절에서 필터링하는 결과와 같음으로 보통 On절은 외부 조인에서 사용한다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
     * join on - on 절에서 조인 대상을 필터링한다. 내부조인의 on절은 where절과 같음으로
     *              보통 외부조인에서 사용한다. 
     */
    public void joinOn() {
        String jpql = "select m "
                + "from MemberJPQL m left join m.team t on m.username = '여성게1' "
                ;
     
        TypedQuery<MemberJPQL> query = em.createQuery(jpql,MemberJPQL.class);
        List<MemberJPQL> members = query.getResultList();
     
        System.out.println("================joinOn=================");
     
        for(MemberJPQL m : members) {
            System.out.println(m.toString());
        }
     
        System.out.println("================joinOn=================");
     
    }
    cs




    패치조인(fetch Join)



    패치조인은 SQL에 존재하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한번에 같이 조회하는 기능이다.


    패치조인 문법 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로 ([]는 선택사항이다)


    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
    /*
     * fetch join - 프로젝션의 엔티티말고도 해당 엔티티의 연관된 모든 엔티티를 모두 조회한다.
     * 만약 회원을 지연로딩 설정하였어도 페치조인이기 때문에 실제엔티티 객체를 set한다.(그냥 조인은 MemberJPQL엔티티를 프록시나 
    아직 초기화되기전인 컬렉션레퍼를 반환)
     * 
     * 주의사항 - 만약 티스토리1 이라는 팀에 연관된 멤버수가 n명이다라고 가정하면,
     * 데이터베이스에서는 총 n개의 로우가 결과로 생기게 된다. 즉, 영속성컨텍스트에도 
     * 결과리스트로 n개의 티스토리1 팀 엔티티가 리턴된다. 이러면 메모리 낭비일텐데....
     * 
     * 티스토리1 엔티티 -> 회원1,회원2
     * 티스토리1 엔티티 -> 회원1,회원2
     * 
     * 이럴때는 select distinct t from TeamJPQL t join fetch t.members where t.name='티스토리1'
     * 로 조회하면 된다. distinct는 2가지의 역할을 한다. 데이터베이스에 쿼리를 날릴때의 키워드로 붙고 나머지 하나는
     * 애플리케이션 단에서 영속성엔티티에게 중복결과를 제거하라는 명령이다. 이말은 즉슨, 위와같은 상황에서 데이터베이스는
     * 각 로우데이터가 다르기 때문에 영향이 없지만, 영속성컨텍스트에서는 티스토리1이라는 중복되는 엔티티들의 결과가 생기기때문에
     * 중복을 제거하여 List결과를 내보내준다.
     * 
     * 티스토리1 엔티티 -> 회원1,회원2
     * 티스토리1 엔티티 -> 회원1,회원2(x)
     * 
     * 즉, FetchType.LAZY로 설정하고 필요할때 fetch조인을 이용해 즉시조회하는 것이 애플리케이션단에 무리를 덜주게된다.
     * 
     */
    public void fetchJoin() {
        //컬렉션 그래프 탐색을 할때는 반드시 별칭이 있어야한다.
        //별칭이 없다면 t.members까지가 최대 탐색이다.
        String jpql = "select t from TeamJPQL t join fetch t.members";
        
        TypedQuery<TeamJPQL> query = em.createQuery(jpql,TeamJPQL.class);
        List<TeamJPQL> teams = query.getResultList();
     
        System.out.println("================fetchJoin=================");
     
        for(TeamJPQL m : teams) {
            System.out.println(m.toString());
        }
     
        System.out.println("================fetchJoin=================");
     
    }
    cs



    JPQL문을 보면 t.members로 패치조인한 것이 눈에 보일 것이다. 다른 엔티티를 조인한 것이 아니고 자기자신의 연관필드로 조인을 한것이다. 그리고 패치조인에서는 조인대상이 되는 연관필드에 별칭을 넣을 수 없다. 즉, 사용이 불가한 것이다.


    위의 JPQL은 밑의 SQL로 실행된다.


    =>


    SELECT

    M.*,T.*

    FROM TEAM_JPQL T

    INNER JOIN MEMBER_JPQL M ON M.TEAM_ID=T.ID


    팀엔티티는 프로젝션에 포함되지 않았는데, SQL은 팀엔티티까지 조회하게 된다.


    위의 주석에도 설명했지만, 회원을 조회할 때 패치조인을 이용하여 팀엔티티를 같이 조회했을 경우에는 아무리 팀엔티티가 지연로딩(FetchType.LAZY)여도 프록시로 반환되는 것이 아니라 실제 엔티티가 반환되고 해당 엔티티가 영속성컨텍스트에 저장된다는 점이다.




    컬렉션 패치조인 (Collection fetch Join)의 문제점과 해결방법



    위에서 패치조인을 이용하여 팀엔티티의 연관 컬렉션인 회원엔티티를 한번에 조회했다. 하지만 이 조회에는 조금 문제가 있다.



    위 그림과 같이 데이터베이스는 같은 팀테이블이지만 데이터가 다른 2개의 로우를 반환하고 있고, 그에 따라 JPA도 2개의 팀엔티티를 결과 리스트로 반환한다. 여기서 생각해보면 팀 테이블은 로우의 데이터가 다르니 2개의 반환값을 갖는 것이 이해되지만 애플리케이션 단의 JPA에서는 같은 팀 엔티티 객체를 꼭 2개를 반환할 필요는 없을 것 같다. 여기서 DISTINCT를 사용한다.




    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     
    public void fetchJoin() {
        String jpql = "select distinct t from TeamJPQL t join fetch t.members where t.name='티스토리1'";
     
        TypedQuery<TeamJPQL> query = em.createQuery(jpql,TeamJPQL.class);
        List<TeamJPQL> teams = query.getResultList();
     
        System.out.println("================fetchJoin=================");
     
        for(TeamJPQL m : teams) {
            System.out.println(m.toString());
        }
     
        System.out.println("================fetchJoin=================");
     
    }
    cs





    바뀐 것은 select 다음에 distinct를 넣어준 것이다. 이 명령어가 하는 역할은 SQL에 DISTINCT명령어를 추가하는 것은 물론이고 애플리케이션단에서 한번 더 중복을 제거한다. 하지만 이 예제에서 SQL에 DISTINCT명령어가 추가되더라도 달라질 것은 없다. 왜냐? 두개의 로우의 데이터가 다르기 때문이다. 하지만 애플리케이션단에서는 다르다 중복된 팀엔티티 결과를 하나로 줄여주는 역할을 한다.




    마지막으로 패치조인의 사용전략이다. 보통 애플리케이션에서 최적화를 위해 글로벌 로딩 전략으로 즉시 로딩(FetchType.EAGER)으로 설정하면 애플리케이션 전체에 불필요한 즉시로딩이 일어난다. 물론 일부는 빠를수도 있지만 전체적으로 봤을 경우엔 성능에 최악이다. 그래서 글로벌 로딩 전략으로는 지연로딩으로 설정하고 필요할때 패치조인을 이용한다면 애플리케이션 성능에 있어 훨씬 효과적일 것이다.


    패치조인 특징


    1. 패치조인 대상에는 별칭을 줄수 없다.

    2. 둘이상의 컬렉션을 패치조인 할 수 없다.

    3. 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다.




    경로표현식



    쉽게 말하면 쿼리에서 .(점)을 찍어 객체 그래프를 탐색하는 것이다.

    ex) m.username, m.team, t.name ....


    <경로표현식 용어 정리>


    상태필드(state field) 

    단순히 값을 저장하기 위한 필드(스칼라타입이되는 필드라 생각하면될듯) 

    연관 필드(association field) 

    연관관계를 위한 필드, 임베디드 타입 필드도 포함한다.

    -단일 값 연관필드 : @ManyToOne,@OneToOne

    -컬렉션 값 연관필드: @OneToMany,@ManyToMany 




    <경로표현식 특징>


    상태필드경로 

    경로탐색의 끝이다. 더는 탐색할 수 없다. 

    단일 값 연관 경로 

    묵시적으로 내부조인이 일어난다. 계속해서 탐색가능하다. 

    컬렉션 값 연관 경로 

    묵시적으로 내부조인이 일어난다. 위와는 다르게 더이상 탐색불가하다. 단 FROM절에서 조인을 통해 별칭을 얻는다면 계속해서 탐색가능하다. 



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
     * 묵시적 조인이다. 
     * 명시적으로 join문을 넣지 않아도 내부적으로 경로탐색(t.members)을 통하여 조인을한다.
     * t.membser.size에서 size는 하나의 키워드(참조가 아니다.)이고, 내부적으로 sql에 count함수로 들어간다.
     * 만약 밑처럼 members라는 컬렉션의 경로탐색을 더하고 싶으면 명시적인 조인으로 컬렉션 연관필드에 별칭을 줘야한다.
     */
    public void ImplicitJoin2() {
        String jpql = "select t.members.size from TeamJPQL t where t.name = '티스토리1'";
        
        TypedQuery<Integer> query = em.createQuery(jpql,Integer.class);
        List<Integer> teams = query.getResultList();
     
        System.out.println("================Implicit=================");
     
        for(Object m : teams) {
            System.out.println(m.toString());
        }
     
        System.out.println("================Implicit=================");
     
    }
    cs



    위소스코드를 보면 "컬렉션은 더이상 참조가 불가능하다면서?"라는 생각이 들것이다. 하지만 t.members.size에서 size는 참조가 아니고 하나의 함수라고 생각하면된다. 


    그렇다면 위에서 컬렉션을 더 참조하고 싶다면 


    SELECT m.username FROM TeamJPQL t join t.members m 


    처럼 명시적인 조인을 해주어야 연관 컬렉션의 그래프 탐색이 가능하다.



    <경로 탐색을 사용한 묵시적 조인 시 주의사항>

    1. 항상 내부 조인이다.

    2. 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적인 조인구문이 들어가야한다.

    3. 경로 탐색은 주로 select, where 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM절에 영향을 준다.

    4. 조인은 성능상 차지하는 이슈가 크다. 그리고 묵시적 조인은 예상하기 힘든 조인이기에 왠만하면 조인은

    명시적조인으로 표현해주는 것이 좋다.












    posted by 여성게
    :
    Web/JPA 2019. 2. 9. 13:44

    JPA - JPQL(객체지향쿼리)


    jpa-study.zip

    (예제소스파일/ jpql package참조)

    JPQL은 가장 중요한 객체지향 쿼리 언어이다. Criteria나 QueryDSL은 결국 JPQL을 편리하게 사용하도록 도와주는 기술이므로 JPA로 데이터베이스 엑세스를 다룬다면 JPQL은 꼭 필수라고 생각이 든다. SQL과 꼭 닮은 쿼리 언어이며 SQL은 데이터 중심의 쿼리라고 하면 JPQL은 엔티티를 대상으로 하는 쿼리 언어라고 할 수 있다. 결국 JPA에서 해당 JPQL을 분석한 다음 적절한 SQL로 변환해주어서 데이터베이스에서 데이터를 가져오는 것이다.

    JPQL 특징

    1. 엔티티 객체를 조회하는 객체지향 쿼리이다.(테이블 대상이 아니다.)
    2. JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.(데이터베이스 방언(Dialect)만 변경하면 소스 수정없이 데이터베이스 변경가능)
    3. SQL보다 간결하다.






    SELECT문


     


    간단한 Select문이다. 일단 from절은 절대 테이블명이 아닌 엔티티명을 넣어주어야한다.(@Entity에서 name을 정의하지 않았다면 테이블명) 그리고 where절 혹은 프로젝션(select 다음 조회할 목록을 지칭) 쪽에서 엔티티의 필드를 참조하기 위해서는 반드시 클래스 내부에 정의된 필드 네임을 그대로 가야한다. 또한 from절 다음 엔티티의 별칭을 꼭 필수로 줘야한다. jqpl을 작성한 이후 쿼리를 생성할때는 2개의 객체를 반환 받을 수 있다. TypeQuery와 Query 클래스이다. 두개의 차이점은 주석에 명시되어있다. 



    TypeQuery, Query


    위에서 설명한 것과 같이 반환타입이 명확한가? 명확하지 않은가? 에 따라 두개중 하나의 쿼리 객체를 이용한다.




    파라미터 바인딩(parameter bind)


    1. 이름 기준 파라미터 - 이름 기준으로 파라미터를 구분하는 방법 ":"를 사용한다.

    2. 위치 기준 파라미터 - 위치 기준으로 파라미터를 구분하는 방법 "?"를 사용한다.



    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
    /*
     * SELECT문
     * 1. 엔티티와 속성은 대소문자를 구분한다. but JPQL keyword는 대소구분을 않는다.
     * 2. Member는 테이블명이 아닌, 클래스명도 아닌 엔티티 명이다.
     * 3. 엔티티명에 별칭은 필수값으로 사용해야한다.
     */
    public void select(){
            String jpql="select m from MemberJPQL m where m.username = '여성게'";
            /*
             * TypeQuery - 반환타입이 명확할때
             * Query - 반환타입이 명확하지 않을때
             */
            TypedQuery<MemberJPQL> query=em.createQuery(jpql,MemberJPQL.class);
     
            /*
             * getResultList() - 결과를 리스트로 받는다.(결과가 없으면 빈 컬렉션을 반환.)
             * 
             * getSingleResult() - 결과가 정확히 하나임을 기대할때 사용한다.
             *                      - 결과가 없으면 javax.persistence.NoResultException
             *                      - 결과가 하나 이상이면
                    javax.persistence.NonUniqueResultException
             */
            List<MemberJPQL> resultList=query.getResultList();
     
            System.out.println("================select=================");
     
            for(MemberJPQL m:resultList){
                System.out.println(m.toString());
            }
     
            String jpql2="select m.username,m.age from MemberJPQL m where m.username = '여성게'";
            Query query2=em.createQuery(jpql2);
            List resultList2=query2.getResultList();
     
            for(Object o:resultList2){
                Object[]result=(Object[])o;
                for(Object o2:result){
                    System.out.println("result element => "+o2);
                }
            }
            System.out.println("================select=================");
    }
    cs



    위치기준 파라미터 바인딩보다 이름기준으로 바인딩하는 것이 더 명확함으로 이름기준 파라미터 바인딩 사용을 하는 것이 나을 듯 싶다.

    프로젝션(projection)


    SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다. [SELECT {프로젝션 대상} FROM~] 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자,문자 등 기본 데이터 타입을 뜻한다(엔티티 클래스에서 int,Long,String 등의 필드)

    여기서 하나 얘기할 것이 있다면 임베디드 타입 프로젝션은 절대 조회의 시작점이 될 수 없다. 즉, From절에서 엔티티를 정의하고 select에서 엔티티.임베디드필드로 참조해야한다.(SELECT m.embeddedTypeField FROM MemberEntity m ~)



    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
    /*
     * projection - SELECT 절에 조회할 대상을 지정하는 것.
     *
     * ex) SELECT {projection} FROM ~
     *
     * 1.엔티티
     * 2.임베디드
     * 3.스칼라(숫자,문자 등 기본 데이터타입)
     */
    public void projection() {
            /*
             * 엔티티타입 프로젝션 - 조회한 엔티티는 영속성컨텍스트에서 관리된다.
             */
            String jpql = "select m.team from MemberJPQL m where m.username = '여성게1'";
            TypedQuery<TeamJPQL> query = em.createQuery(jpql,TeamJPQL.class);
            List<TeamJPQL> resultList = query.getResultList();
     
            System.out.println("================projection=================");
     
            for(TeamJPQL t : resultList) {
                System.out.println(t.toString());
            }
     
            /*
             * 임베디드타입 프로젝션 - 임베디드 타입은 조회의 시작점이 될 수 없기에 Order엔티티로 조회를 시작해
             *                      그다음 임베디드 타입을 참조한다.
             */
            String jpql2 = "select o.address from OrderJPQL o where o.product.name ='맥북1'";
            TypedQuery<AddressJPQL> query2 = em.createQuery(jpql2,AddressJPQL.class);
            List<AddressJPQL> resultList2 = query2.getResultList();
     
            for(AddressJPQL o : resultList2) {
                System.out.println(o.toString());
            }
     
            /*
             * 스칼라타입 - 여러종류의 스칼라타입을 받기 위해서는 TypeQuery를 사용할 수 없다.
             * 해결책 - new 명령어를 사용한 DTO 타입변환
             * 반드시 밑 문자열에 들어간 파라미터의 순서대로 해당 dto에 생성자가 존재해야한다.
             */
            String jpql3 = "select new com.spring.jpa.jpql.MemberDTO(m.username,m.age)"
            + " from MemberJPQL m where m.username = '여성게1'";
            TypedQuery<MemberDTO> query3 = em.createQuery(jpql3,MemberDTO.class);
            List<MemberDTO> members = query3.getResultList();
     
            for(MemberDTO dto : members) {
                System.out.println(dto.toString());
            }
     
            System.out.println("================projection=================");
    }
    cs



    위에서 조금 특이한 것이 있다면 jpql3이다. 조회할 대상이 여러개이면 TypeQuery로는 조회할 수 없기 때문에 하나의 DTO를 만들어서 위와 같이 "new" 명령어를 사용해서 결과를 DTO로 받아 TypeQuery 객체를 이용할 수 있다.



    페이징 API


    페이징을 하기 위해서는 다소 지루하고 반복적인 일이다. 게다가 데이터베이스 구현체에 따라 문법도 다르다. 하지만 JPA는 모든 데이터베이스에서 동일하게 사용할 수 있는 페이징 메소드를 제공해준다.



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public void paging() {
            String jpql = "select m from MemberJPQL m ";
            TypedQuery<MemberJPQL> query = em.createQuery(jpql,MemberJPQL.class);
            //11부터 조회
            query.setFirstResult(10);
            //20개 11~30까지 조회
            query.setMaxResults(20);
            List<MemberJPQL> members = query.getResultList();
     
            System.out.println("================paging=================");
     
            for(MemberJPQL m : members) {
                System.out.println(m.toString());
            }
     
            System.out.println("================paging=================");
    }
     
    cs



    현 예제는 값을 직접 입력해주었지만, 추후에는 컨트롤러 단에서 페이징에 대한 데이터를 직접 주입받아서 사용하지 않을까 싶다.(Pagable 클래스)






    집합과 정렬



    함수 

    설명 

    COUNT 

    결과 수를 구한다. 반환타입:Long 

    MAX,MIN 

    최대,최소 값을 구한다. 

     AVG

    평균값을 구한다. 반환타입: Double 

     SUM

    합을 구한다.

    정수합 반환타입 : Long

    소수합 반환타입 : Double

    BigInteger,BigDecimal합 반환타입 : BigInteger,BigDecimal  



    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
    public void groupFunction() {
            String jpql = "select count(m) from MemberJPQL m ";
            Query query = em.createQuery(jpql);
            Long count = (Long) query.getSingleResult();
     
            System.out.println("================groupFunction=================");
            System.out.println("count = "+count);
            System.out.println("================groupFunction=================");
    }
     
    public void groupByHaving() {
            String jpql = "select t.name, count(m.age) "
                        + "from MemberJPQL m left join m.team t "
                        + "group by t.name ";
     
            Query query = em.createQuery(jpql);
            List members = query.getResultList();
     
            System.out.println("================groupByHaving=================");
     
            for(Object o : members) {
                Object[] result = (Object[]) o;
                for(Object o2 : result) {
                    System.out.print("result element => "+o2);
                }
                System.out.println();
            }
            System.out.println("================groupByHaving=================");
    }
     
    cs



    집합함수 사용시 참고사항


    1.Null 값은 무시하므로 통계에 잡히지 않는다.

    2.만약 값이 없는데 집합함수를 사용한다면 Null값이 된다.(단 count는 0)

    3.DISTINCT를 집합함수 안에 사용하여 중복된 값을 제거하고 집합을 구할 수 있다.

      (ex select count(distinct m.age) from MemberJPQL m)

    4.DISTINCT를 count에서 사용할 때 임베디드타입 필드는 지원하지 않는다.




    여기까지 JPQL포스팅을 한번 끊고 양이 많아 JPQL 조인부터는 다음 포스팅에 정리하겠습니다. :)







    posted by 여성게
    :