35. 객체지향 쿼리 언어(2)
35. 객체지향 쿼리 언어(2)
2. JPQL
여기서는 JPQL의 사용 방법 위주로 설명하겠다. 시작하기 전에 JPQL의 특징을 다시 한 번 정리해보자.
- JPQL은 객체지향 쿼리 언어이다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
시작하기 전에 이번 절에서 예제로 사용할 도메인 모델을 살펴보자.
위의 그림 샘플 UML과 위의 그림 샘플 ERD를 보자. 실무에서 사용하는 주문 모델링은 더 복잡하지만 JPQL의 이해가 목적이므로 단순화했다. 여기서는 회원이 상품을 주문하는 다대다 관계라는 것을 특히 주의해서 보자. 그리고 Address는 임베디드 타입인데 이것은 값 타입이므로 UML에서 스테레오 타입을 사용해 <<Value>>로 정의했다. 이것은 ERD를 보면 ORDERS 테이블에 포함되어 있다.
1. 기본 문법과 쿼리 API
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다. 참고로 엔티티를 저장할 때는 EntityManager.persist() 메소드를 사용하면 되므로 INSERT 문은 없다.
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
위의 예제의 JPQL 문법을 보면 전체 구조는 SQL과 비슷한 것을 알 수 있다. JPQL에서 UPDATE, DELETE 문은 벌크 연산이라 하는데 아래 절에서 설명하겠다. 지금부터 SELECT 문을 자세히 알아보자.
SELECT 문
SELECT 문은 다음과 같이 사용한다.
SELECT m FROM Member AS m where m.username = 'Hello'
대소문자 구분
엔티티와 속성은 대소문자를 구분한다. 예를 들어 Member, username은 대소문자를 구분한다. 반면에 SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
엔티티 이름
JPQL에서 사용한 Member 클래스는 클래스 명이 아니라 엔티티 명이다. 엔티티 명은 @Entity(name="XXX")로 지정할 수 있다. 엔티티 명을 지정하지 않으면 클래스 명을 기본값으로 사용한다. 기본값인 클래스 명을 엔티티 명으로 사용하는 것을 추천한다.
별칭은 필수
Member AS m을 보면 Member에 m이라는 별칭을 주었다. JPQL은 별칭을 필수로 사용해야 한다. 따라서 다음 코드처럼 별칭 없이 작성하면 잘못된 문법이라는 오류가 발생한다.
SELECT username FROM MEMBER m // 잘못된 문법, username을
// m.username으로 고쳐야 한다.
AS는 생략할 수 있다. 따라서 Member m처럼 사용해도 된다.
하이버네이트는 JPQL 표준도 지원하지만 더 많은 기능을 가진 HQL(Hibernate Query Language)을 제공한다. JPA 구현체로 하이버네이트를 사용하면 HQL도 사용할 수 있다. HQL은 SELECT username FROM Member m의 usrename처럼 별칭 없이 사용할 수 있다.
JPA 표준 명세는 별칭을 식별 변수(Identification variable)라는 용어로 정의햇다. 하지만 보통 별칭(alias)이라는 단어가 익숙하므로 별칭으로 부르겠다.
TypeQuery, Query
작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체는 TypeQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 된다. 아래 예제를 보자.
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
em.createQuery()의 두 번째 파라미터에 반환할 타입을 지정하면 TypeQuery를 반환하고 지정하지 않으면 Query를 반환한다. 조회 대상이 Member 엔티티이므로 조회 대상 타입이 명확하다. 이때는 아래 예제처럼 TypeQuery를 사용할 수 있다.
Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();
for (Object o : resultList) {
Object[] result = (Object[]) o; // 결과가 둘 이상이면 Object[] 반환
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
위의 예제는 조회 대상이 String 타입인 회원 이름과 Integer인 나이이므로 조회 대상 타입이 명확하지 않다. 이처럼 SELECT 절에서 여러 엔티티나 컬럼을 선택할 때는 반환할 타입이 명확하지 않으므로 Query 객체를 사용해야 한다.
Query 객체는 SELECT 절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환하고 SELECT 절의 대상이 하나면 Object를 반환한다. 예를 들어 SELECT m.username from Member m이면 결과를 Object로 반환하고 SELECT m.username, m.age from Member m이면 Object[]를 반환한다.
두 코드를 비교해보면 타입을 변환할 필요가 없는 TypeQuery를 사용하는 것이 더 편리한 것을 알 수 있다.
결과 조회
다음 메소드들을 호출하면 실제 쿼리를 실행해서 데이터베이스를 조회한다.
- query.getResultList() : 결과를 예제로 반환한다. 만약 결과가 없으면 빈 컬렉션을 반환한다.
- query.getSingleResult() : 결과가 정확히 하나일 때 사용한다.
- 결과가 없으면 javax.persistence.NoResultException 예외가 발생한다.- 결과가 1개보다 많으면 javax.persistence.NonUniqueResultException 예외가 발생한다.
getSingleResult()는 결과가 정확히 1개가 아니면 예외가 발생한다는 점에 주의해야 한다.
Member member = query.getSingleResult();
2. 파라미터 바인딩
JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다.
이름 기준 파라미터
이름 기준 파라미터(Named parameters)는 파라미터를 이름으로 구분하는 방법이다. 이름 기준 파라미터는 앞에 :를 사용한다.
String usernameParam = "User1";
TypedQuery<Member> query = em.createQuery("SELECT M FROM MEMBER m where m.username = :username", Member.class);
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
위의 예제의 JPQL을 보면 :username이라는 이름 기준 파라미터를 정의하고 query.setParameter()에서 username이라는 이름으로 파라미터를 바인딩한다. 참고로 JPQL API는 대부분 메소드 체인 방식으로 설계되어 있어서 다음과 같이 연속해서 작성할 수 있다.
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = :username",
Member.class)
.setParameter("username", usernameParam)
.getResultList();
위치 기준 파라미터
위치 기준 파라미터(Positional parameters)를 사용하려면 ? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작한다. 아래 예제를 보자.
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = ?1,
Member.class)
.setParameter(1, usernameParam)
.getResultList();
위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.
JPQL을 수정해서 다음 코드처럼 파라미터 바인딩 방식을 사용하지 않고 직접 문자를 더해 만들어 넣으면 악의적인 사용자에 의해 SQL 인젝션 공격을 당할 수 있다. 또한 성능 이슈도 있는데 파라미터 바인딩 방식을 사용하면 파라미터의 값이 달라도 같은 쿼리로 인식해서 JPA는 JPQL을 SQL로 파싱한 결과를 재사용할 수 있다. 그리고 데이터베이스도 내부에서 실행한 SQL을 파싱해서 사용하는데 같은 쿼리는 파싱한 결과를 재사용할 수 있다. 결과적으로 애플리케이션과 데이터베이스 모두 해당 쿼리의 파싱 결과를 재사용할 수 있어서 전체 성능이 향상된다. 따라서 파라미터 바인딩 방식은 선택이 아닌 필수다.
// 파라미터 바인딩 방식을 사용하지 않고 직접 JPQL을 만들면 위험하다. "select m from Member m wheere m.username = '" + usernameParam + "'"
3. 프로젝션
SELECT 절에 조회할 대상을 재정하는 것을 프로젝션(projection)이라 하고 [SLECT {프로젝션 대상} FROM]으로 대상을 선택한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.
엔티티 프로젝션
SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
처음은 회원을 조회했고 두 번째는 회원과 연관된 팀을 조회했는데 둘 다 엔티티를 프로젝션 대상으로 사용했다. 쉽게 생각하면 원하는 객체를 바로 조회한 것인데 컬럼을 하나하나 나열해서 조회해야 하는 SQL과는 차이가 있다. 참고로 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
임베디드 타입 프로젝션
JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다. 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다. 다음은 임베디드 타입인 Address를 조회의 시작점으로 사용해서 잘못된 쿼리다.
String query = "SELECT a FROM Address a";
다음 코드에서 Order 엔티티가 시작점이다. 이렇게 엔티티를 통해서 임베디드 타입을 조회할 수 있다.
String query = "SELECT o.address FRM Order o";
List<Address> addresses = em.createQuery(query, Address.class)
.getResultList();
실행된 SQL은 다음과 같다.
select
order.city,
order.street,
order.zipcode
from
Orders order
임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들은 스칼라 타입이라 한다. 예를 들어 전체 회원의 이름을 조회하려면 다음처럼 쿼리하면 된다.
List<String> usernames = em.createQuery("SELECT username FROM Member m", String.class)
.getResultList();
중복 데이터를 제거하려면 DISTINCT를 사용한다.
SELECT DISTINCT username FROM Member m
다음과 같은 통계 쿼리도 주소 스칼라 타입으로 조회한다. 통계 쿼리용 함수들은 뒤에서 설명하겠다.
Double orderAmountAvg = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class)
.getSingleResult();
여러 값 조회
엔티티를 대상으로 조회하려면 편리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다. 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다. 아래 예제를 보자.
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
제네릭에 Object[]를 사용하면 다음 코드처럼 조금 더 간결하게 개발할 수 있다. 아래 예제를 살펴보자.
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
for (Object[] row : resultList) {
String username = (String) row[0];
Integer age = (Integer) row[1];
}
스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다. 아래 예제를 보자.
List<Object[]> resultList = em.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
.getResultList();
for (Object[] row : resultList) {
Member member = (Member) row[0]; // 엔티티
Product product = (Product) row[1]; // 엔티티
int orderAmount = (Integer) row[2]; // 스칼라
}
물론 이때도 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
NEW 명령어
아래 예제는 username, age 두 필드를 프로젝션에서 타입을 지정할 수 없으므로 TypeQuery를 사용할 수 없다. 따라서 Object[]를 반환받았다. 실제 애플리케이션 개발시에는 Object[]를 직접 사용하지 않고 아래 예제의 UserDTO처럼 의미 있는 객체로 변환해서 사용할 것이다.
List<Object[]> resultList = em.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
// 객체 변환 작업
List<UserDTO> userDTOs = new ArrayList<UserDTO>();
for (Object[] row : resultList) {
UserDTO userDTO = new UserDTO((String)row[0], (Integer)row[1]);
userDTOs.add(userDTO);
}
return userDTOs;
public class UserDTo {
private String username;
private int age;
public UserDTO(String username, int age) {
this.username = username;
this.age = age;
}
// ...
}
이런 객체 변환 작업은 지루하다. 이번에는 아래 예제처럼 NEW 명령어를 사용해보자.
TypedQuery<UserDTO> query = em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m" UserDTO.class);
List<UserDTO> resultList = query.getResultList();
SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다. 그리고 NEW 명령어를 사용한 클래스로 TypeQuery를 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있다.
NEW 명령어를 사용할 때는 다음 2가지를 주의해야 한다.
- 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
- 순서와 타입이 일치하는 생성자가 필요하다.
참고
- 자바 ORM 표준 JPA 프로그래밍
Author And Source
이 문제에 관하여(35. 객체지향 쿼리 언어(2)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@jsj3282/35.-객체지향-쿼리-언어2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)