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

2021. 4. 17. 01:47 Spring Data/Spring Data JPA

JPA - JPQL(객체지향쿼리)

 

jpa-study.zip
0.13MB

(예제소스파일/ 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. 위치 기준 파라미터 - 위치 기준으로 파라미터를 구분하는 방법 "?"를 사용한다.

 

/*
 * 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=================");
}

 

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

 

프로젝션(projection)

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

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

/*
 * 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=================");
}

 

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

 

페이징 API

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

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=================");
}

 

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

 

집합과 정렬

 

함수 

설명 

COUNT 

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

MAX,MIN 

최대,최소 값을 구한다. 

 AVG

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

 SUM

합을 구한다.

정수합 반환타입 : Long

소수합 반환타입 : Double

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

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=================");
}

 

집합함수 사용시 참고사항

 

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

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

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

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

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

 

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


출처: https://coding-start.tistory.com/86?category=781616 [코딩스타트]