JPA/JPQL

[JPA] JPQL이란?

IT록흐 2023. 6. 13. 15:30
반응형

 

 

JPA는 객체지향Application과 관계형DB 사이의 패러다임 불일치 문제를 해결한다. 

 

 

[JPA] 패러다임 불일치

어플리케이션 개발은 주로 객체지향언어로 이루어진다. 객체지향언어는 현실의 많은 문제를 코드로 구현하지만 문제가 있다. 데이터는 주로 관계형DB에 저장되는데 관계형DB와 어플리케이션은

lordofkangs.tistory.com

 

JPA는 APP과 DB사이에서 자동으로 SQL문을 만들어 준다. 개발자는 객체지향 관점만 유지하면 된다. 그러나 JPA도 한계가 있다. 모든 쿼리를 객체지향방식으로 표현할 수 없다. 특히나 SELECT문은 조회에 여러 조건이 붙는 경우도 많고 조건이 동적으로 변하기도 한다. 그러므로 이를 커버할 무언가가 필요하다. 그것이 JPQL( Java Persistence Query Language )이다.

 

JPQL

JPQL은 SQL 포맷은 유지하되, 테이블이 아닌 엔티티를 대상으로 한다. 

 

SELECT문 = SELECT절 FROM절 [ WHERE 절 ] [GROUPBY 절 ] [ HAVING 절 ] [ ORDERBY 절 ]

 

JPQL문 생성부터 실행까지 3단계로 나뉜다.

 

1. JPQL문 작성

2. JPQL문 실행객체 생성

3. JPQL문 실행 후 결과반환

 

3가지 과정을 하나씩 알아보자.

 

 

1. JPQL문 작성

 

집합 ( GROUP BY, HAVING )

#작성된 JPQL문
String jpql = "SELECT p.category, AVG(p.price) FROM Product p " +
              "GROUP BY p.category " +
              "HAVING AVG(p.price) > :averagePrice";

 

엔티티를 기준으로 JPQL문을 만들면 JPA(하이버네이트)가 자동으로 SQL문으로 변환한다.

그러므로 개발자는 엔티티 관점만 유지하면 된다. 

 

JPQL문을 보자. FROM 뒤에 Product는 엔티티이다. Product p처럼 별칭은 필수로 붙여주는 것이 좋다. 별칭을 붙여주어야 필드이름이 중복되었을 때, JPA가 구분할 수 있다. 위 JPQL문은 category 속성을 기준으로 그룹화하여 price의 평균(AVG)을 출력한다.  HAVING은 GROUP BY로 그룹화 된 상태에서의 조건문이 필요할 때 사용된다. 

 

:averagePrice는 동적 파라미터 대상을 의미한다. 이번 포스팅 마지막에 동적 파라미터를 다룰 예정이니 그때 자세히 알아보겠다.

 

 

정렬 ( ORDER BY )

String jpql = "SELECT e FROM Employee e " +
              "ORDER BY e.lastName ASC, e.firstName DESC";

 

정렬도 ORDER BY, DESC, ASC를 그대로 가져왔다.  이와 같이, 포맷은 SQL문을 그대로 가져왔다.

 

 

2. JPQL 쿼리 생성 ( TypedQuery, Query )

 

그럼 위에서 만든 문자열 쿼리를 실제로 실행가능한 형태로 만들어 해보자. 문자열 쿼리를 실행가능한 형태로 만드는 주체는 엔티티매니저이다. 엔티티메니저에게 JPQL문과 반환타입을 파라미터로 넘기면 쿼리객체를 생성한다. 쿼리객체 타입은 2가지가 있다.

 

TypedQuery, Query

 

두 쿼리객체 중 무엇을 사용할지는 SELECT문이 무엇을 반환하는지에 따라 결정된다.

 

// 반환타입이 명확할때 ( 제네릭 )
TypedQuery<Member> query =
em.createQuery("SELECT m FROM Member m", Member.class); 

// 반환타입이 불명확할 때
Query query =
em.createQuery("SELECT m.username, m.age from Member m");

 

 

SELECT문으로 조회되는 결과는 2가지 형태로 분류할 수 있다. 

 

1) 엔티티, 임베디드 타입, DTO 같은 특정 '대상'

 

엔티티, 임베디드 타입은 명확한 클래스 형태로 존재한다. 만약 조회목적이 엔티티나 임베디드 타입 같이, 명확하고 정해진 대상이라면 TypedQuery를 사용하는 것이 좋다. TypedQuery는 타입안정성을 보장하기에 오류를 사전에 발견 할 수 있다. 

 

String jpql = "SELECT e FROM Employee e";
TypedQuery<Employee> query = entityManager.createQuery(jpql,Employee.class);
List<Employee> resultList = query.getResultList(); // Employee 형태로 반환받음

 

DTO는 조금 특별하다. SELECT 문에 new 연산자를 사용한다.

SELECT new com.example.dto.EmployeeDTO(e.id, e.name) FROM Employee e

 

DTO를 간단히 설명하면, 엔티티는 APP과 DB 사이의 소통을 목적으로 존재한다. 굉장히 민감한 영역이라 할 수 있다. 엔티티는 최대한 내부에 감추어야 한다. 그러므로 서비스 계층에 데이터를 넘길때는 DTO, 즉, Data Transfer Object를 사용한다. 엔티티가 외부로 넘어가면 엔티티의 내부정보가 노출될 위험이 있기 때문이다. 

 

그래서 SELECT한 결과를 바로 DTO 객체로 만드는 방법이 new연산자를 SELECT문에 삽입하는 방법이다. new연산자와 패키지명을 포함한 DTO 클래스 경로를 조합하여 SELECT문을 만든다. DTO클래스는 이에 맞는 생성자도 가지고 있어야 한다. 

 

String jpql = "SELECT new com.example.dto.EmployeeDTO(e.id, e.name) FROM Employee e";
TypedQuery<EmployeeDTO> query = entityManager.createQuery(jpql, EmployeeDTO.class);
List<EmployeeDTO> resultList = query.getResultList();

 

 

2) 스칼라 타입 ( 필드 )

 

스칼라 타입은 단일한 값을 가진 데이터 타입을 의미한다. 엔티티의 필드가 대표적이다. 만약 SELECT문이 회원이 아닌 회원의 '나이'를 출력한다면 명확히 정해진 타입이 없다. 엔티티에 따라, 나이는 실수형, 정수형 그리고 문자열도 될 수 있다. 명확한 클래스 형태로 정해져있지 않은 결과를 반환하는 경우, TypedQuery는 부적절하다. 

 

이런 경우, Query 객체를 사용하는 것이 좋다. 

String jpql = "SELECT e.firstName, e.lastName FROM Employee e";
Query query = entityManager.createQuery(jpql);
List<Object[]> resultList = query.getResultList();

for (Object[] result : resultList) {
    String firstName = (String) result[0];
    String lastName = (String) result[1];
	//...
}

 

 

 

3. 결과조회API

 

JPQL 실행객체를 생성하였으니 이제 실행할 차례이다. 실행한 후 결과를 return하는 조회 API는 두 가지가 있다. 

 

1) query.getSingleResult() : 결과가 정확히 하나인 경우에만 사용

2) query.getResultList() : 하나 이상의 결과를 반환할 때 사용

 

getSingleResult()

String jpql = "SELECT e FROM Employee e WHERE e.id = :id";
TypedQuery<Employee> query = entityManager.createQuery(jpql, Employee.class);
query.setParameter("id", 1L);
Employee result = query.getSingleResult();

 

PK로 조회하는 경우는 결과가 하나이다. 이런 경우, 결과가 2개이면 안된다. 만약 2개 이상이 반환된다면 NonUniqueResultException이 발생한다. 예외처리를 통하여, 오류에 대처할 수 있다. 

 

getResultList()

String jpql = "SELECT e FROM Employee e";
TypedQuery<Employee> query = entityManager.createQuery(jpql,Employee.class);
List<Employee> resultList = query.getResultList(); // List로 반환

 

 

하나 이상의 결과를 반환하는 getResultList()는 List 형태로 결과를 반환한다. 

 

 


 

 

여기까지 JPQL의 가장 기본적인 세 단계 실행과정을 알아보았다. 번외로 파라미터 바인딩을 살펴보고 마무리하겠다.

 

파라미터 바인딩

 

JPQL문에 동적으로 값을 넘기는 방법이다. 이는 1단계, 2단계에서 설정된다.

 

String jpql = "SELECT e FROM Employee e WHERE e.id = :id"; // :id
TypedQuery<Employee> query = entityManager.createQuery(jpql, Employee.class);
query.setParameter("id", 1L); //파라미터 설정 
Employee result = query.getSingleResult();

 

개발자는 JPQL문을 작성할 때, 동적파라미터 값이 들어갈 자리를 ':' 부호를 사용하여 표시한다. 그리고 실행객체를 생성할 때 파라미터를 위와 같이 세팅하므로써 파라미터 바인딩을 구현할 수 있다.

 

 


 

 

참고자료

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

 

 

 

 

반응형

'JPA > JPQL' 카테고리의 다른 글

[JPA] 묵시적 조인보다는 명시적 조인  (0) 2023.06.15
[JPA] 쿼리결과 변환하기 ( JPQL 함수 )  (0) 2023.06.14
[JPA] 서브쿼리 ( SubQuery )  (0) 2023.06.14
[JPA] JOIN 과 JOIN FETCH의 차이  (0) 2023.06.14
[JPA] 페이징 API  (0) 2023.06.13