[자바 표준 ORM JPA]10장 객체지향 쿼리 언어 - 1

이번 포스팅에서는

  • 객체지향 쿼리 소개
  • JPQL
  • Criteria
  • QueryDSL 을 다루고 나머지는 다음 포스팅에서 하도록 하겠습니다.

객체지향 쿼리 소개

  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.

JPA는 (JPQL, Criteria, QueryDSL, JDBC, MyBatis)등을 지원해준다. 지금부터 알아가보자~

JPQL

  • JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다.
  • JPQL은 결국 SQL로 변환된다.

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

엔티티를 저장할 때는 EntityManager.persist() 메소를 사용하면 되므로 INSERT문은 없다.

기본 문법과 쿼리 API

SELECT

  • 대소문자 구분

    JPQL 키워드를 제외한 엔티티와 속성은 대소문자를 구분한다.

  • 엔티티 이름

    Member는 클래스 명이 아니라 엔티티 명이다. 기본값인 클래스 명을 엔티티 명으로 사용하는 것을 추천한다.

  • 별칭은 필수

    Member AS m처럼 JPQL은 별칭을 필수로 사용해야 한다. AS는 생략할 수 있다.

TypedQuery, Query 작성한 JPQL을 실행하려면 쿼리를 만들어야 한다. 반환할 타입을 명확하게 지정할 수 있으면 TypedQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 있으면 Query 객체를 사용하면 된다.

파라미터 바인딩

JDBC는 위치 기준 파라미터 바인딩만 지원하지만 JPQL은 이름 기준 파라미터 바인딩도 지원한다.

  • 이름 기준 파라미터
    TypedQuery<Member> query = 
    em.createQuery("select m from Member m where m.username = :username",
                  Member.class);
    

    파라미터를 이름으로 구분하는 방버 앞에 : 를 사용한다.

  • 위치 기준 파라미터
    TypedQuery<Member> query = 
    em.createQuery("select m from Member m where m.username = :username",
                  Member.class);
    

    ? 다음에 위치 값을 주면 된다. 위치값은 1부터 시작한다.

프로젝션

SELECT 절에 조회할 대항을 지정하는 것을 프로젝션이라 한다.

  • 엔티티 프로젝션

    SELECT m FROM m // 회원

    SELECT m.team FROM Member m // 팀

  • 임베디드 타입 프로젝션

    임베디드 타입은 엔티티와 거의 비슷하게 사용되지만 조회의 시작점이 될 수 없다는 제약이 있다.

  • 스칼라 타입 프로젝션

    숫자, 문자, 날짜와 같은 기본 데이터 타입을 스칼라 타입이라고 한다. 통계 쿼리도 주로 스칼라 타입으로 조회한다.

  • NEW 명령어

    NEW 명령어를 사용한 클래스로 지루한 객체 변환 작업을 줄일 수 있다. 사용시 다음 2가지를 주의해야 한다.

    1. 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
    2. 순서와 타입이 일치하는 생성자가 필요하다.

페이징 API

페이징 처리용 SQL은 지루하고 반복적인데다가 데이터 베이스마다 처리하는 SQL이 다르다.

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

  • setFirstResult(int startPosition): 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult): 조회할 데이터 수
TypedQuery<Member> query = 
  em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC",
                Member.class);
  query.setFirstResult(10)
  query.setMaxResults(20)
  query.getResultList();

시작은 10이므로 11번째부터 시작해서 총 20건의 데이터를 조회한다. 따라서 11~30번 데이터를 조회한다.

집합과 정렬

select
    COUNT(m), //회원수
    SUM(m.age), //나이 합
    AVG(m.age), //평균 나이
    MAX(m.age), //최대 나이
    MIN(m.age)  //최소 나이
from Member m

참고사항

  • NULL 값은 무시하므로 통계에 잡히지 않는다(DISINCT가 정의되어 있어도 무시된다).
  • 만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다. 단 COUNT는 0이 된다.
  • DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합을 구할 수 있다.
  • DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.

GROUP BY, HAVING

select t.name, count(m.age), sum(m.age), avg(m.age), max(m.age), min(m.age)
from Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING avg(m.age) >= 10

그룹별 통계 데이터 중 평균나이가 10살 이상인 그룹

JPQL 조인

JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.

내부조인

INNER JOIN을 사용한다. INNER은 생략 가능

String query = "SELECT m FROM Member m INNER JOIN m.team t "
  + "WHERE t.name = :teamName";

JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 것이다.

  • FROM Member m: 회원을 선택하고 m이라는 별칭을 주었다.
  • Member m JOIN m.team t: 회원이 가지고 있는 연관 필드로 팀과 조인한다. 조인한 팀에는 t라는 별칭을 주었다.

만약 조인한 두 개의 엔티티를 조회하려면 다음과 같이 JPQL을 작성하면 된다.

select m, t
from Member m join m.team t

외부 조인

select m
from Member m left [outer] join m.team t 

세타 조인

WHERE 절을 사용해서 세타 조인을 할 수 있다. 세타조인은 내부 조인만 지원한다. 세타 조인을 사용하면 전혀 관계 없는 엔티티도 조인할 수 있다.

// JPQL
select count(m) from Member m, Team t
where m.username = t.name

// SQL
SELECT COUNT(M.ID)
FROM MEMBER M CROSS JOIN TEAM T
WHERE M.USERNAME=T.NAME

JOIN ON 절

ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다. 참고로 내부 조인의 결과는 ON 절 = WHERE 절이므로 보통 외부 조인에서만 사용된다.

// JPQL
select m, t from Member m
left join m.team t on t.name = 'A'
  
// SQL
select m.*, t.* from Member m
left join Team t on m.team_id and t.name='A'

패치 조인 패치 조인은 SQL에서 이야기하는 조인의 종류가 아니라 JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이다. join fetch 명령어로 사용할 수 있다.

하이버네이트는 패치 조인에도 별칭을 허용한다.

패치 조인과 DISTINCT

JPQL의 DISTINCT 명령어는 SQL에 DISTINCT를 추가하고 애플리케이션에서 한 번 더 중복을 제거한다.

select distinct t
from Team t join fetch t.members
where t.name = '팀A'

select distinct t는 팀 엔티티의 중복을 제거하라는 것이다. 따라서 이제 ‘팀A’는 하나만 조회된다.

teamname = 팀A, team = Team@0x100
->username = 회원1, member = Member@0x200
->username = 회원2, member = Member@0x300

패치 조인과 일반 조인의 차이

JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다. 만약 회원 컬렉션을 지연 로딩으로 설정하면 프록시나 아직 초기화하지 않은 컬렉션 래퍼를 반환한다. 즉시 로딩으로 설정하면 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한 번 더 실행한다.

반면에 페치 조인을 사용하면 연관된 엔티티도 같이 조회한다.

패치 조인의 특징과 한계

페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 조회할 수 있어서 성능을 최적화할 수 있다.

페치 조인은 글로벌 로딩 전략보다 우선한다. 예를 들어 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회한다.

최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하면 애플리케이션 전체에서 항상 즉시 로딩이 일어난다. 일부는 빠를 수 있어도 전체로 보면 자주 사용하지 않는 엔티티를 자주 로딩하므로 오히려 성능에 악영향을 미칠 수 있다. 따라서 되도록 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.

페치 조인은 다음과 같은 한계가 있다.

  • 페치 조인 대상에는 별칭을 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.

서브 쿼리

JPQL에선 서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 하이버네이트는 추가적으로 SELECT 절의 서브 쿼리도 허용한다.

-- 나이가 평균보다 많은 회원 예제
select m from Member m
where m.age > (select avg(m2.age) from Member m2)

서브 쿼리 함수

조건식

image image

  • [NOT] EXISTS : 서브 쿼리에 결과가 존재하면 참
  • { ALL ANY SOME } :
    • ALL: 조건을 모두 만족하면 참
    • ANY 혹은 SOME: 조건을 하나라도 만족하면 참
  • [NOT] IN : 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참

번외로 springboot JPA 쿼리 메소드 조건시긍ㄹ 추가하고 마무리 하겠다.

https://velog.io/@mineru/JPA-JPA-쿼리-메소드-조건식이-기억이-나지-않을-때

Categories:

Updated:

Leave a comment