[JPA 기본편] 9. 객체 지향 쿼리( JPQL ) - 기본 문법

HJ·2024년 2월 26일
0

JPA 기본편

목록 보기
9/10
post-thumbnail

김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 보고 작성한 내용입니다.


1. JPA 가 지원하는 쿼리

1-1. JPQL

JPA 를 사용하면 엔티티 객체를 중심으로 개발을 하기 때문에, 검색할 때 테이블이 아닌 엔티티 객체를 대상으로 검색해야 합니다.

그래서 JPA 는 SQL 을 추상화한 JPQL 이라는 객체 지향 쿼리 언어를 지원하는데 SQL을 추상화했기 때문에 특정 데이터베이스 SQL 에 의존하지 않습니다.

또 JPQL 을 작성하면 SQL 로 번역되어 실행되는데 ANSI 표준 SQL 이 지원하는 모든 문법을 지원하며 엔티티 객체를 대상으로 쿼리합니다. ( SQL 은 테이블 대상 )


1-2. JPA Criteria

JPQL 은 문자로 작성해야 하기 때문에 동적 쿼리를 작성하기 어렵고, 쿼리가 실행되는 런타임에 오류가 발생할 수도 있습니다.

Criteria 는 쿼리를 코드로 작성할 수 있어 컴파일 시점에 오류를 파악할 수 있고, JPQL 보다 동적쿼리를 작성하기 편리합니다. 하지만 코드가 복잡하고 가독성이 좋지 않습니다.


1-3. QueryDSL

Criteria 와 동일하게 JPQL 빌더 역할을 하며, 코드로 작성할 수 있어 컴파일 시점에 오류를 파악할 수 있습니다. 무엇보다 단순하고 쉬우며 동적 쿼리 작성도 편리합니다.


1-4. 네이티브 SQL

JPA 에서 SQL 을 직접 사용하는 기능인데 createNativeQuery() 를 사용하며, JPQL 로 해결할 수 없는 특정 데이터베이스에 의존적인 기능이 필요할 때 사용합니다.


1-5. JDBC API 직접 사용

JPA 를 사용하면서 JDBC API 를 직접 사용하거나, Spring JdbcTemplate, MyBatis 를 함께 사용할 수 있습니다.

이때 SQL 을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시하는 것처럼 영속성 컨텍스트를 적절한 시점에 강제로 플러시 하는게 필요합니다.




2. JPQL 기본 문법

2-1. JPQL 형태

select m from Member m where m.age > 20
select COUNT(m), SUM(m.age), MAX(m.age) from Member m

JPQL 에서 select, from 과 같은 키워드는 대소문자를 구분하지 않지만 엔티티를 표현할 때는 대문자를 사용하고, 속성을 표현할 때는 소문자를 사용합니다.

select 절에 위와 같은 집계 함수 사용이 가능합니다.

from 절에는 엔티티 이름을 사용하며, 별칭은 필수이나 as 는 생략할 수 있습니다. 엔티티 이름이란 클래스명이 아닌 @Entity 에 지정된 이름이며, 기본은 클래스명과 동일합니다.



2-2. TypedQuery, Query

createQuery() 를 사용했을 때 반환되는 타입은 TypedQueryQuery 가 있습니다.

[ TypedQuery ]

TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> str = em.createQuery("select m.name from Member m", String.class);

createQuery() 의 두 번째 파라미터로 응답 클래스에 대한 타입 정보를 넘겨줄 수 있는데 이때 반환되는 것이 TypedQuery 이며 반환 타입이 명확할 때 사용됩니다.

첫 번째 예시는 Member 엔티티를 반환하기 때문에 Member 를 가지고, 두 번째 m.name 은 String 이기 때문에 String 타입을 주었고 제네릭에 String 이 있는 것을 확인할 수 있습니다.


[ Query ]

Query query = em.createQuery("select m from Member m");
Query query2 = em.createQuery("select m.name, m.age from Member m");

반대로 Query 는 두 번째 파라미터로 엔티티 정보를 넘겨주지 않았을 때, 반환 타입이 명확하지 않을 때 사용됩니다.

두 번째 예시를 보면 m.namem.age 를 사용했는데 name 은 String 이고, age 는 int 입니다. 두 개의 타입이 다르기 때문에 타입 정보를 넘겨줄 수 없고, 이때 Query 가 반환됩니다.



2-3. 결과 조회 API

TypedQuery<Member> query = em.createQuery("select m from Member m",Member.class);
List<Member> members = query.getResultList();
Member resultMember = query.getSingleResult();

getResultList() 는 결과가 하나 이상일 때 사용하며, 리스트를 반환합니다. 결과가 없으면 빈 리스트가 반환됩니다.

getSingleResult() 는 결과가 정확히 하나일 때 사용하며, 단일 객체를 반환합니다. 만약 결과가 없거나 둘 이상이면 예외가 발생합니다.



2-4. 파라미터 바인딩

em.createQuery("select m from Member m where m.name = :username" ,Member.class)
                .setParameter("username", "kim");

em.createQuery("select m from Member m where m.username = ?1", Member.class)
                .setParameter(1, "kim");

파라미터 바인딩하는 첫 번째 방법은 이름을 기준으로 하는 방법입니다. 파라미터 이름 앞에 : 를 붙여서 나타내며 setParameter() 를 통해 지정할 수 있습니다.

두 번째 방법은 위치를 기준으로 하는 방법인데 ?위치 로 JPQL 을 작성하면 되는데 이 방법은 추천하지 않습니다.



2-5. 프로젝션

프로젝션이란 select 절에 조회할 대상을 지정하는 것인데 엔티티, 연관관계 엔티티, 임베디드 타입 숫자와 문자 같은 스칼라 타입을 지정할 수 있습니다.

[ 엔티티 프로젝션 ]

em.createQuery("select m from Member m", Member.class);
em.createQuery("select m.team from Member m", Team.class);
em.createQuery("select t from Member m join m.team t", Team.class);

엔티티 프로젝션의 경우, 조회되는 데이터 모두 영속성 컨텍스트에서 관리됩니다.

m.team 을 조회하는 경우, createQuery() 의 두 번째 파라미터로 Team.class 를 넘겨주어야 합니다. 그렇게 되면 Member 와 Team 이 조인되는 쿼리가 실행됩니다.

참고로 두 번째 예시와 세 번째 예시는 동일한 SQL 이 실행되는데, 두 번쨰의 경우 JOIN 예측이 안되기 때문에 세 번째처럼 사용하는 것이 좋습니다.


[ 임베디드, 스칼라 ]

em.createQuery("select o.address from Order o", Address.class); // 임베디드
em.createQuery("select a from Address a", Address.class); // 불가능한 예시
em.createQuery("select m.name from Member m", Member.class);  // 스칼라

Order 내부에 임베디드 타입을 조회하려면 응답 타입을 임베디드 타입으로 지정해야 합니다. 임베디드 타입은 하나의 테이블에 컬럼이 있는 형태이기 때문에 문제가 없습니다.

임베디드 타입은 특정 테이블에 소속되어 있기 때문에 두 번째 예시처럼 사용할 수 없습니다.



2-6. 여러 타입 조회

여러 개의 타입을 조회해야 하는 경우가 있는데 이때 3가지 방법이 존재합니다.

[ Query ]

List resultList = em.createQuery("select m.username, m.age from Member m")
                    .getResultList();
Object obj = resultList.get(0);
Object[] result = (Object[]) obj;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

첫 번째는 Query 를 사용하는 방법입니다. 위의 List 안에는 Object 가 들어 있습니다. 조회 결과로 반환되는 것은 두 개이기 때문에 이를 Object[] 로 캐스팅하고, 배열 내부에서 조회 결과를 가져올 수 있습니다.


[ Object[] ]

List<Object[]> resultList = em.createQuery(
                    "select m.username, m.age from Member m")
                    .getResultList();
Object[] result = resultList.get(0);
System.out.println("username = " + objects[0]);
System.out.println("age = " + objects[1]);

두 번째는 Object[] 를 사용하는 방법입니다. 반환되는 List 의 제네릭을 Object[] 로 지정하면 캐스팅 작업 없이 바로 Object[] 를 받을 수 있습니다.


[ new ]

List<MemberDTO> dto = em.createQuery(
                        "SELECT new jpabook.jpql.MemberDTO(m.username, m.age) " +
                        "FROM Member m", MemberDTO.class)
                    .getResultList();

엔티티가 아닌 다른 타입으로 조회를 하는 경우 반드시 new 키워드를 사용해서 생성해야 합니다. 생성할 때는 패키지명을 포함한 전체 클래스명을 입력해야 하며, 순서와 타입이 일치하는 생성자가 필수로 필요합니다.



2-7. 페이징

em.createQuery("select m from Member m order by m.age desc", Member.class)
   .setFirstResult(10)
   .setMaxResults(20)
   .getResultRest();

setFirstResult() 로 조회 시작 위치를 지정할 수 있으며, 0 부터 시작합니다.

setMaxResults() 로 조회할 데이터의 수를 지정할 수 있습니다.

위의 함수를 작성하면 JPA 가 각 데이터베이스 방언에 맞게 쿼리를 작성해서 실행하게 됩니다.




3. 조인

JPQL 은 엔티티를 중심으로 동작하기 때문에 객체 스타일로 조인 문법을 작성해야 합니다.

3-1. 내부조인, 외부조인

// 내부조인
String jpql = "select m from Member m inner join m.team t";
em.createQuery(jpql, Member.class);
// 외부조인
String jpql = "select m from Member m left outer join m.team t";
em.createQuery(jpql, Member.class);

inner 와 outer 는 생략할 수 있습니다.


3-2. 세타조인

세타조인은 전혀 연관관계가 없는 엔티티를 조인하는 것입니다. 아래 예시들은 Member 가 4명, Team 이 2개가 저장되어 있고, 둘의 연관관계는 없는 상태입니다.

[ 조건 X ]

String jpql = "select m, t from Member m, Team t";
List resultList = em.createQuery(jpql).getResultList();
System.out.println("resultList.size() = " + resultList.size());   // 결과 : 8

위의 예시에서는 조인 조건이 없으므로 카테시안 곱으로 인한 모든 데이터가 함께 나오게 되기 때문에 8이 출력됩니다.


[ 조건 O ]

String jpql = "select m, t from Member m, Team t where m.username = t.name";
List resultList = em.createQuery(jpql).getResultList();
System.out.println("resultList.size() = " + resultList.size());   // 결과 : 2

만약 여기서 조건을 추가해서 실행하면 2가 출력됩니다. 왜냐하면 세타조인은 내부조인만 가능하기 때문입니다. 전체 8개 중에 Member 와 Team 이 동일한 것은 최대 Team 의 데이터 수만큼 가능하기 때문에 결과가 2가 나오게 됩니다.


3-3. ON 절

[ 조인 대상 필터링 ]

String jpql = "select m, t from Member m left join m.team t on t.name = 'A'";

JPA 는 조인할 때 조인 대상을 필터링 할 수 있는 ON 절을 지원합니다.
위의 예시는 회원과 팀을 조인할 때 팀의 이름이 A 인 팀만 조인하는 JPQL 입니다.


[ 연관관계 없는 외부조인 ]

String jpql = "select m, t from Member m left join m.team t on m.name = t.name";

위의 세타조인에서는 내부조인만 가능했는데 연관관계 없는 엔티티 외부 조인도 가능합니다.




4. JPA 지원 함수

4-1. 서브쿼리

  • [NOT] EXISTS : 서브쿼리에 결과가 존재하면 참

  • [NOT] IN : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

  • { ALL | ANY | SOME }

    • ALL 은 조건을 모두 만족해야 참

    • ANY 와 SOME 은 조건을 하나라도 만족하면 참

원래는 select 절, where 절, having 절에서만 서브쿼리를 사용할 수 있었는데 하이버네이트 6 부터는 FROM 절 서브쿼리도 지원합니다.


4-2. 타입

[ ENUM ]

// JPQL 에 직접 사용
String jpql = "select m from Member m where m.type = jpabook.MemberType.ADMIN";

// 파라미터 바인딩 사용
String jpql = "select m from Member m where m.type = :userType";
em.createQuery(jpql, Member.class)
    .setParameter("userType", MemberType.ADMIN);

jpql 에 ENUM 타입을 사용할 수 있는데 JPQL 자체에 작성할 때는 패키지명을 포함해서 적어주어야 합니다. 파라미터 바인딩을 사용해서 ENUM 타입을 사용할 수 있습니다.


[ 엔티티 타입 ]

String jpql = "select i from Item i where type(i) = Book";
em.createQuery(jpql, Item.class);

이전 예시에서 Item 를 상속 받는 엔티티가 Book, Album, Movie 가 있었는데 그 중에 Book 만 가지고 오고 싶을 때 위처럼 type() 을 사용할 수 있습니다.

Item 에는 자식들을 구분하기 위한 DTYPE 컬럼이 추가되는데 위의 쿼리가 실행되면 where 절에 item.DTYPE = 'BOOK' 이 추가됩니다.


4-3. 조건식 Case

[ 기본 Case 식 ]

String jpql =
        "select " +
        "case when m.age < 8 then '어린이' " +
        "     when m.age < 20 then '학생' " +
        "     else '성인' " +
        "end " +
        "from Member m";
List<String> resultList = em.createQuery(jpql, String.class).getResultList();

[ 단순 Case 식 ]

String jpql =
        "select " +
        "case t.name " +
        "     when 'teamA' then '인센티브110%' " +
        "     when 'teamB' then '인센티브120%' " +
        "     else '인센티브105%' " +
        "end " +
        "from Team t";
List<String> resultList = em.createQuery(jpql, String.class).getResultList();

[ COALESCE ]

select coalesce(m.username,'이름 없는 회원') from Member m

COALESCE 는 하나씩 조회해서 NULL 이 아니면 반환합니다. 위의 예시는 사용자 이름이 없으면 "이름 없는 회원" 을 반환합니다.


[ NULLIF ]

select NULLIF(m.username, '관리자') from Member m

NULLIF 는 두 값이 같으면 null 을 반환하고, 다르면 첫 번째 값을 반환합니다. 위의 예시는 사용자 이름이 "관리자"면 null 을 반환하고, 나머지는 본인의 이름을 반환합니다.


4-4. 기타 함수들

IN, AND, OR, NOT, BETWEEN, LIKE, IS NULL, CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE, ABS, SQRT, MOD, SIZE, INDEX 와 같은 기본 표현과 함수들도 다 사용할 수 있습니다.

DB 내부에 존재하는 사용자 정의 함수를 사용할 때는 함수 등록이 필요한데 Hibernate 6 버전 부터는 방식이 변경되었다고 한다 ( 참고 )

profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글