'객체지향쿼리'에 해당되는 글 2건

  1. 2019.02.12 :: JPA - 영속성 컨텍스트와 JPQL
  2. 2019.02.09 :: JPA - JPQL(객체지향쿼리),Java Persistence Query Language
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. 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 여성게
: