본문 바로가기
[ ORM ]/JPA

[ JPA ] Java Persistence Query Language(JPQL , 객체 지향 쿼리 언어)(1)

by 환이s 2024. 4. 3.


Intro

 

JPA는 엔티티 객체를 중심으로 개발하기 때문에

검색 쿼리 실행하면 테이블 대상이 아닌 엔티티 객체를 대상으로 요청을 보내야 한다.

 

하지만 모든 데이터베이스 데이터를 객체로 변환해서 검색하는 것은 불가능하며

애플리케이션이 필요한 데이터만 가져오려면 결국 검색 조건이 포함된 SQL을 사용해야 한다.

 

오늘은 검색 조건에 엔티티 객체를 대상으로 요청을 보내기 위한 객체 지향 쿼리 언어에 대해 알아보자.


JPQL(Java Persistence Query Language)

 

JPQL은 SQL을 추상화하여 사용하는 객체지향 쿼리 언어이다.

 

따라서 테이블을 대상으로 하지 않고 엔티티 객체를 대상으로 쿼리를 수행하며

추상화를 통해서 특정 데이터베이스 SQL에 의존되지 않게 개발할 수 있다.

 

실제 수행할 때는 JPQL로 작성한 쿼리가 매핑 정보 등을 통해서 SQL로 변환되어 DB에 수행된다.

 

JPQL의 문법은 SQL과 유사하다.

다만 다른 점은 테이블의 이름이 아니라 엔티티 이름을 사용한다는 것이다.

 

간단한 예시를 통해서 알아보자.

예시로는 나이가 20살 이상인 회원을 모두 검색하기 위한 쿼리를 어떻게 작성하는지 알아보자.

em.createQuery("select m from Member as m where m.age > 20", Member.class);

 

 

위 예시에서 사용된 Member 클래스는 테이블이 아닌 엔티티 클래스의 이름이며

JPQL에서 엔티티와 엔티티의 속성(Member, age)은 대소문자를 구분한다.

 

여기서 주의할 점은 엔티티를 사용하는 경우에는 엔티티의 별칭을 무조건 지정해주어야 한다.

위 예시를 살펴보면 Member 엔티티의 별칭을 m으로 지정해 주었다. 

 

앞서 언급했듯이 JPQL 문법은 SQL과 유사하다는 걸 알 수 있다.

참고로 as 키워드는 제외해도 된다.

 

지금까지 간단하게 특징에 대해 알아봤지만 밑에 예제 시나리오에서 좀 더 다룰 예정이다.

 

정리해 보면

JPQL은 객체지향 쿼리 언어다.

따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리 한다.

그로 인해 JQPL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않고 SQL로 변환된다. 

 

이번 포스팅에서 비중을 많이 차지하는 문법이라서 또 다른 예제를 통해서 확실하게 알아가자.


JPQL(Java Persistence Query Language) - 예제 시나리오

 

먼저 아래 그림을 살펴보자.

 

 

위 그림은 예제 시나리오에 사용될 간단한 주문 모델링인 UML과 ERD이다.

 

실무에서는 주문 모델링이 더 복잡하지만 JPQL의 사용법이 목적이기 대문에 단순화했다.

여기서 주의할 점은 회원이 상품을 주문하는 N:N(다대다) 관계라는 것이다.

또한 Address(주소)는 임베디드 타입인 값 타입이므로 UML에서 스테레오 타입을 사용해서 <<Value>>로 정의했다.

 

ERD를 보면 ORDERS 테이블에 포함되어 있는 걸 확인할 수 있다.

 

[ 기본 문법과 쿼리 API ]

 

위에서 언급했지만 다시 한번 개념을 잡아가자면

JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다.

select_문 :: =
select_절
from_절
[where_절]
[groupby_절][having_절]
[orderby_절]

update_문 :: = update_절 [where_절]
delete_문 :: = delte_절 [where_절]

 

참고로 INSERT문은 엔티티를 저장할 때 EntityManager.persist() 메서드를 사용해서 저장할 수 있어서 제외된다.

 

위의 JPQL 문법을 보면 SQL과 전체 구조가 비슷한 것을 알 수 있다.

JQPL에서 UPDATE, DELETE문벌크 연산이라 하는데 이는 다음 포스팅에서 다룰 예정이다.

 

■SELECT 문 

 

SELECT 문은 다음과 같이 사용한다.

SELECT m FROM Member m where m.username = 'kim'

■ 대소문자 구분

 

엔티티와 속성은 대소문자를 구분한다.

예를 들어 Member, username은 대소문자를 구분하지만, SELECT, FROM, AS 같은 JPQL 키워드는 대소문자를 구분하지 않는다.

 


■ Entity Name

 

JPQL에서 사용한 Member는 클래스 이름이 아니라 엔티티 명이다.

엔티티 명은 @Entity(name="Member")로 지정할 수 있다.

 

이를 지정하지 않으면 기본값이 고정인데, 되도록이면 기본값으로 사용하는 것을 추천한다.

 


■별칭

 

위 회원 이름 조회 쿼리인 "From Member m"을 보면 m이라는 별칭을 주었다.

JPQL은 별칭을 필수로 사용해야 하며 AS는 생략할 수 있기 때문에 위 예제처럼 사용해도 된다.

 


■결과 조회 API(TypeQuery, Query)

 

그렇다면 위 예제 코드를 실행하려면 어떻게 해야 할까?

 

작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다.

 

쿼리 객체는 TypeQuery와 Query가 있는데

TypeQuery는 반환할 타입을 명확하게 지정할 수 있을 때 사용하고,

Query는 반환 타입을 명확하게 지정할 수 없을 때 사용한다.

 

예제 코드로 알아보자.

//TypedQuery

TypedQuery<Member> typedQuery = em.createQuery("SELECT m FROM Member m", Member.class);

List<Member> resultList = typedQuery.getResultList();

for (Member member : resultList) {
	System.out.println("member = " + member);
}

//Query

Query query = em.createQuery("SELECT m.username, m.age from Member m");
List resultList = query.getResultList();

for(Object obj : resultList) {
	// 결과가 둘 이상이면 Object[] 반환
	Object[] list = (Object[]) obj; 
    System.out.println("username = " + result[0]);
    System.out.println("age = " + result[1]);
}

 

 

위 예제를 토대로 알아보자면

em.createQuery()에 추가로 파라미터에 반환할 타입을 지정하면 TypeQuery를 반환하고

지정하지 않으면 Query를 반환한다.

 

조회 대상이 Member 엔티티이므로 조회 대상 타입이 명확하다.

이때는 TypeQuery를 사용할 수 있다.

 

반대로 Query 예제 코드를 보면

조회 대상이 String 타입인 회원 이름과 Integer 타입인 나이가 조회 대상이므로 명확하지 않다.

 

이처럼 SELECT 절에서 여러 엔티티나 컬럼을 선택할 때는

반환할 타입이 명확하지 않으므로 Query 객체를 사용해야 한다.

 

Query 객체는 SELECT 절의 조회 대상이 예제처럼 둘 이상이면 Object[]를 반환하고

반대로 SELECT절의 조회 대상이 하나면 Object를 반환한다.

 

두 코드들을 비교해 보면 타입을 변환할 필요가 없는 TypeQuery를

사용하는 것이 더 편리한 것을 알 수 있다.

 

그럼 위에서 작성한 query.getResultList();는 어떤 역할을 할까?

 

위에서 조회 쿼리를 작성했다면

결과를 조회하기 위해 작성한 쿼리를 데이터베이스를 조회할 때 사용하는 메서드이다.

 

결과를 조회할 때 사용되는 메서드는 위에서 사용된 getResultList()getSingleResult()라는 메서드가 존재한다.

query.getResultList();

query.getSingleResult();

 

두 메서드는 각자의 특징을 갖고 있는데 정리하자면 다음과 같다.

 

  • getResultList()
    • 결과를 예제로 반환한다.
    • 만약 결과가 없으면 빈 컬렉션을 반환한다.
  • getSingleResult()
    • 결과가 정확히 하나일 때 사용한다.
    • 결과가 없으면 javax.persistence.NoResultException 예외가 발생한다.
    • 반대로 결과가 1개보다 많으면 javax.persistence.NonUniqueResultException 예외가 발생한다.

 

지금까지 정리해 보면 

JPQL 문법과 각 메서드의 사용법 및 조회(Select)하는 방법에 대해 알아봤다.

 

하지만 아직 부족하다.

전체 조회 할 때는 괜찮지만 만약 회원을 1명 찾고 싶다면? 나이가 18살 이상인 회원만 조회하고 싶다면?

파라미터를 바인딩시켜야 한다.

 

바로 다음으로 알아보자.


파라미터 바인딩

 

파라미터 바인딩은 쿼리에 작성되는 특정 속성을 매개변수로 매핑하는 것을 말하며

쿼리에 매개변수를 매핑하는 방식에는 이름을 기준으로 하는 방식과 위치를 기준으로 하는 방식이 있다.

 

다음 코드를 보면서 사용 방법을 이해해 보자.

 

■ 위치 기반

Query query = em.createQuery("select m from Member m where m.username =? 1")
                .setParameter(1, usernameParam);
  • 위치 기준 바인딩은 =? 연산자를 사용한다.
  • 위의 예제 코드에서 확인할 수 있듯이, 메서드 체이닝을 사용할 수 있다.

 

 

■ 이름 기반

Query query = em.createQuery("select m from Member m where m.username =: username")
                .setParameter("username", usernameParam);
  • 이름 기준 바인딩은 =: 연산자를 사용한다.
  • 위의 예제 코드에서 확인할 수 있듯이, 메서드 체이닝을 사용할 수 있다.

 

위 코드에서 확인했듯이 사용법은 두 가지 존재한다.

하지만 매개변수를 바인딩할 때에는 이름 기반 바인딩을 사용하는 것을 권장한다.

 

그 이유는 

위치 기반 바인딩을 사용하면 중간에 새로운 매개변수를 추가하는 경우,

순서가 밀리기 때문이다.

 

또한, 숫자를 통해서 어떤 위치의 매개변수가 무엇을 의미하는지 쉽게 파악할 수 없다.

실무에서 사용하면 가독성이 떨어지고 유지보수 상황에서 비효율적이다.


Projection(프로젝션)

 

프로젝션은 SELECT 절에 조회할 대상을 지정하는 것을 의미하며

"SELECT [프로젝션 대상] FROM"으로 대상을 선택한다.

 

위 예제에서는

select m from Member

 

m이 프로젝션 대상인 것이다.

 

프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입이 있는데,

스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 말한다.

 

엔티티 프로젝션

SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀

 

위 조회 쿼리를 보면

처음에는 Member를 조회하고 두 번째는 Member와 연관된 team을 조회했는데

둘 다 엔티티를 프로젝션 대상으로 사용했다.

 

즉, 원하는 객체를 바로 조회하는 것인데

컬럼을 하나씩 나열해서 조회해야 하는 SQL과 차이가 있다.

이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.


임베디드 타입 프로젝션

 

JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다.

하지만, 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.

 

먼저 잘못된 쿼리를 알아보자.

String query = "SELECT a FROM Address a";

 

위 쿼리는 Address를 조회하는 잘못된 쿼리이다.

 

왜 잘못된 쿼리일까?

그 이유는 위에서 지정된 예제 시나리오를 보면 알 수 있다.

 

Address는 임베디드 타입이다.

그렇기에 직접적인 조회를 할 수 없다.

 

(이 부분에서 이해가 안 된다면 아래 포스팅을 먼저 보고 오는 것을 권장한다.)

 

[ JPA ] 값 타입(Value Object)

Intro JPA의 데이터 타입을 분류하면 엔티티 타입과 값 타입(Value Object)으로 구분할 수 있다. 엔티티 타입은 @Entity 애노테이션으로 정의하는 객체, @Entity를 붙여서 관리하던 클래스들이다. 이 타입

drg2524.tistory.com

 

그렇다면 임베디드 타입을 조회하려면 어떻게 해야 할까?

 

그 방법은 바로 ERD를 보면 알 수 있는데

임베디드 타입으로 사용될 컬럼이 있는 Order 테이블이 힌트이다.

 

즉, Order 엔티티를 시작점으로,

지금까지 규칙으로 사용된 엔티티를 통해서 임베디드 타입을 조회할 수 있다.

 

String query = "SELECT orders.address FROM Order orders";
List<Address> addresseList = em.createQuery(query, Address.class).getResultList();

 

정리하자면

임베디드 타입은 엔티티 타입이 아닌 값 타입이므로 직접 조회할 수 없다.

직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

 


스칼라 타입 프로젝션

 

스칼라 타입은 위에서 언급했듯 숫자, 문자, 날짜와 같은 기본 데이터 타입이다.

 

예를 들어보자면

전체 회원의 이름을 조회하려면 다음 쿼리를 참고하면 된다.

List<String> username = em.createQuery("SELECT username FROM Member m", String.class)
									.getResultList();

 

추가로 실무에서는 자주 사용하는 키워드가 있는데, 바로 DISTINCT 키워드이다.

 

DISTINCT는 중복 제거 키워드이며, SELECT 절에 DISTINCT 키워드를 추가해서

검색하면 중복되는 값을 제거할 수 있다.

SELECT DISTINCT username FROM Member m

new 명령어

 

엔티티를 대상으로 조회하면 편리하겠지만,

꼭 필요한 데이터들만 선택해서 조회해야 하는 상황이 발생한다.

 

이러한 상황이 발생했을 때는 MemberDTO처럼 의미 있게 객체로 변환해서 사용할 수 있다.

 

public class MemberDTO{
    
    private String username;
    private int age;
    
    public MemberDTO(String username, int age){
        this.username = username;
        this.age = age;
    }
}

//TypedQuery 

TypedQuery<MemberDTO> query =
    em.createQuery("select new jpabook.jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class);
    
List<MemberDTO> resultList = query.getResultList();

 

SELECT 키워드 다음에 new 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.

 

그리고 new 명령어를 사용한 클래스로 TypedQuery를 사용할 수 있어서 지루한 객체 변환 작업을 줄일 수 있다.

 

하지만 new 명령어를 사용할 때는 2가지를 주의해야 한다.

 

첫 번째는 위 예제 코드에서 보이는 것처럼

패키지 명을 포함한 전체 클래스 명을 입력해야 한다.

 

두 번째는 순서와 타입이 일치하는 생성자가 필요하다.

 

물론 new 명령어를 사용해서 조회하는 방법도 있지만 또 다른 방법도 존재한다.

 

쿼리의 결과가 하나의 값이 아니라 여러 개의 값을 반환하는 경우

다음과 같은 방법들로 결과를 받을 수 있다.

 

query = "select m.username, m.age from Member as m";

// 1. Query 타입으로 조회
List result1 = em.createQuery(query).getResultList();
for(Object obj : result1) {
    Object[] o = (Object[]) obj;
    System.out.println(Arrays.toString(o));
}

// 2. Object[]  타입으로 조회
List<Object[]> result2 = em.createQuery(query).getResultList();
for(Object[] o : result2) {
    System.out.println(Arrays.toString(o));
}

Paging API

 

JPA에서는 DB에서 데이터를 조회할 때 페이징을 사용하여 원하는 구간의 데이터를 가져올 수 있도록 API를 제공한다.

 

예시로 Mybatis를 사용해서 페이징 처리를 했을 때

큰 문제점이 바로 데이터베이스마다 페이징을 처리하는 SQL 문법이 다르다는 점이다.

 

JPA는 페이징을 다음 두 API로 추상화한다.

 

  • setFirstResult(int startPosition) : 조회 시작 위치(start 0)
  • setMaxREsults(int maxResult) : 조회할 데이터 수
// 해당 쿼리로 조회하면 결과의 11번째 데이터부터 최대 20개의 데이터를 리스트로 반환한다.
List<Member> members = em.createQuery("select m from Member m order by m.age desc", Member.class)
                            .setFirstResult(10)
                            .setMaxResults(20)
                            .getResultList();

 

위 코드를 분석하면 10번째 데이터부터 시작이므로 11번째 데이터 20건의 데이터를 조회하므로,

최대 20개의 데이터를 리스트로 반환한다.

 

데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은

데이터베이스 방언 때문이다.

 

이 부분은 첫 포스팅에 작성했으므로 생략하겠다.

 

하지만 만약 페이징을 더 최적화하고 싶다면 

JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 한다.


마치며

 

오늘은 페이징 API 까지만 알아보고 다음 포스팅에 집합과 정렬 섹션부터 이어서 작성해 보겠습니다.

 

다음 포스팅에서 뵙겠습니다.

위 포스팅 글은 김영한님의 자바 ORM 표준 JPA 프로그래밍-기본편을 참고했습니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 | 김영한 - 인프런

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

www.inflearn.com

 

728x90