코딩은 마라톤

[자바 ORM 표준 JPA 프로그래밍] 10장. JPQL (2) 본문

Language/Java

[자바 ORM 표준 JPA 프로그래밍] 10장. JPQL (2)

anxi 2024. 6. 19. 00:25

경로 표현식

경로표현식이란 .(점)을 찍어 객체 그래프를 탐색하는 것이다.

select m.username
from Member m
    join m.team t
    join m.orders o
where t.name = '팀A'

 

위 쿼리에서 m.username, m.team, m.orders, t.name 모두 경로 표현식을 사용한 예다.

 

용어 정리

  • 상태 필드 : 단순히 값을 저장하기 위한 필드 (필드 or 프로퍼티)
  • 연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함 (필드 or 프로퍼티)
    • 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티
    • 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션

경로 표현식과 특징

  • 상태 필드 경로 : 경로 탐색의 끝이다. 더는 탐색할 수 없다.
select m.username, m.age from Member m
  • 단일 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 계속 탐색 가능
    • 묵시적 조인은 모두 내부 조인이다.
    • 명시적 조인 : JPQL에 JOIN을 직접 적어 주는 것
    • 묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 일어남 (하기 코드)
// JPQL
select o.member from Order o

// SQL
select m.*
from Orders o
    inner join Member m on o.member_id=m.id
  • 컬렉션 값 연관 경로 : 묵시적으로 내부 조인이 일어난다. 더는 탐색할 수 없으나 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색 가능
select t.members from Team t // 성공
select t.members.username from Team t // 실패

 

t.members 처럼 컬렉션 까지만 경로 탐색이 가능하며 그 다음 경로를 탐색할 순 없다.

select m.username from Team t join t.members m // t.members에 새로운 별칭 추가

 

또한 컬렉션은 컬렉션 크기를 구할 수 있는 size라는 특별한 기능을 사용할 수 있다. (COUNT 함수)

 

묵시적 조인 시 주의사항

  • 항상 내부 조인이다.
  • 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색 시 명시적 조인을 통해 별칭을 얻어야 한다.
  • 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL FROM 절에 영향을 준다.

따라서 성능이 중요하면 묵시적 조인보다는 명시적 조인을 사용하여 분석하자.


서브 쿼리

서브쿼리는 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.

// 나이가 평균보다 많은 회원을 찾는 경우
select m from Member m
where m.age > (select avg(m2.age) from Member m2)

 

서브 쿼리 함수

  • EXISTS

- 문법 : [NOT] EXISTS (subquery)

- 설명 : 서브쿼리에 결과가 존재하면 참이다.

  • {ALL | ANY | SOME}

- 문법 : {ALL | ANY | SOME} (subquery)

- 비교 연산자와 같이 사용한다. 

  - ALL : 조건을 모두 만족하면 참이다.

  - ANY, SOME : 조건을 하나라도 만족하면 참이다.

// 어떤 팀이든 팀에 소속된 멤버
select m from Member m
where m.team = ANY (select t from Team t)

 

  • IN

- 문법 : [NOT] IN (subquery)
- 설명 : 서브쿼리의 결과 중 하나라도 같은 것이 있음 참이다. IN은 서브 쿼리가 아닌 곳에서도 사용한다.

// 20세 이상 멤버를 가진 팀
select t from Team t
where t IN (select t2 From Team t2 JOIN t2.members m2 where m2.age >= 20)

조건식

타입 표현 (모르는 부분만 정리)

  • 문자 : 작은 따옴표로 표현, 작은 따옴표를 표현하고 싶으면 작음 따옴표 연속 2개 사용(''), 'She''s'
  • Enum : 패키지명을 포함한 전체 이름을 사용한다.
  • 엔티티 타입 : 엔티티의 타입을 표현한다. 주로 상속과 관련해서 사용한다. TYPE(m) = Member

연산자 우선 순위

  1. 경로 탐색 연산 (.)
  2. 수학 연산 : +, -, ...
  3. 비교 연산 : =, >, ... , BETWEEN, LIKE, IN, IS NULL, IS EMPTY, EXISTS...
  4. 논리 연산 : NOT, OR, AND

Between, IN, Like, NULL 비교

  • Between 식
    • 문법 : X [NOT] BETWEEN A AND B
    • 설명 : X는 A ~ B 사이의 값이면 참 (A, B 포함)
  • IN 식
    • 문법 : X [NOT] IN 
    • 설명 : X와 같은 값이 예제에 하나라도 있으면 참
select m from Member M where m.username in ('한', '성')
  • Like 식 (정규표현식 느낌)
    • 문법 : 문자표현식 [NOT] LIKE 패턴값 [ESCAPE 이스케이프 문자]
    • 설명 : 문자표현식과 패턴값을 비교한다.
      • % : 아무 값들이 입력되어도 된다. (값이 없어도 됨)
      • _ : 한 글자는 아무 값이 입력되어도 되지만 값은 있어야 한다
// 회원, A회원..
select m from Member m
where m.username like '%회원'

 

  • NULL 비교식
    • 문법 : {단일값 경로 | 입력 파라미터} IS [NOT] NULL
    • 설명 : NULL인지 비교한다. NULL은 =으로 비교하지 말고 IS NULL을 사용한다.

컬렉션 식

컬렉션에서만 사용하는 기능이다.

  • 빈 컬렉션 비교 식
    • 문법 : { 컬렉션 연관 경로 } IS [NOT] EMPTY
    • 설명 : 컬렉션에 값이 비었으면 참
//JPQL: 주문이 하나라도 있는 회원 조회
select m from Member m
where m.orders is not empty

// is empty 대신 is null을 사용하면 오류!

 

  • 컬렉션의 멤버 식
    • 문법 : { 엔티티, 값 } [NOT] MEMBER [OF] { 컬렉션 값 연관경로 }
    • 설명 : 엔티티나 값이 컬렉션에 포함되어 있음 참
select t from Team t
where :memberParam member of t.members

 

스칼라 식

스칼라는 숫자, 문자, 날짜, 엔티티 타입 등 가장 기본적인 타입들을 말한다.

 

문자 함수

  • CONCAT(문자1, 문자2, ...)
    • 문자를 합한다.
  • SUBSTRING(문자, 위치, [길이])
    • 위치부터 시작해 길이만큼 문자를 구한다. 길이 값이 없으면 나머지 전체 길이를 뜻한다.
    • SUBSTRING('ABCD', 2, 2) = BC
  • TRIM (LEADING, TRAILING, BOTH)
    • LEADING : 왼쪽만
    • TRAILING : 오른쪽만
    • BOTH : 양쪽 다 트림 문제를 제거 (default)
    • TRIM('  SSF ') = SSF
  • LOWER, UPPER : 소문자, 대문자 변경
  • LENGTH : 문자 길이
  • LOCATE(찾을 문자, 원본 문자, [검색시작위치])
    • 검색위치부터 문자를 검색한다. 1부터 시작, 못찾으면 0 반환
    • LOCATE('DE', ABCDE') = 4

수학 함수

  • ABS : 절대값
  • SQRT : 제곱근
  • MOD (수학식, 나눌 수) : 나머지를 구한다.
  • SIZE (컬렉션 값 연관 경로식) : 컬렉션 크기
  • INDEX (별칭) : LIST 타입 컬렉션의 위치값을 구함. 컬렉션이 @OrderColumn을 사용하는 경우에만 사용
    • t.members m where INDEX(M) > 3

날짜 함수

  • CURRENT_DATE : 현재 날짜 | 2024-06-17
  • CURRENT_TIME : 현재 시간 | 23:40:22
  • CURRENT_TIMESTAMP : 현재 날짜 시간 | 2024-06-17 23:40:22.736

CASE 식

특정 조건에 따라 분기할 때 CASE 식을 사용한다.

  • 기본 CASE
    • 문법 : CASE { WHEN <조건식> THEN <스칼라식> } + ELSE <스칼라식> END
    • 예시 : select case when m.age < 20 then '미성년자' else '성인' end from Member m
  • 심플 CASE 
    • 조건식을 사용할 수 없는, switch case 문과 유사하다.
    • 문법 : CASE <조건대상> when ... else... end
  • COALESCE
    • 문법 : COALESCE(<스칼라식> {, <스칼라식> } +)
    • 설명 : 스칼라식을 차례로 조회해서 null이 아니면 반환
    • 예시 : m.username이 null이면 '무명'을 반환할 때
      • selece coalesce(m.username, '무명') from Member m
  • NULLIF
    • 문법 : NULLIF(<스칼라식>, <스칼라식>)
    • 설명 : 두 스칼라 값이 같으면 null을 반환, 그렇지 않으면 첫 번째 스칼라 값 반환, 보통 집합함수와 함께 사용(집합 함수는 null 미포함하기 때문)
    • 예시 : 사용자 이름이 '박지성'이면 null을 반환 그렇지 않으면 m.username을 반환할 때
      • select NULLIF(m.username, '박지성') from Member m

다형성 쿼리

JPQL로 부모 엔티티를 조회하면 자식 엔티티도 함께 조회한다.

// 부모
@Entity
@Inheritance (strategy = InheritanceType. SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {...}

// 자식 (Book, Album, Movie)
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
    private String author;
}

 

TYPE

TYPE은 엔티티 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 사용한다.

 

예 : Item중 Album과 Book을 조회할 때

//JPQL
select i from Item i
where type(i) IN (Album, Book)

//SQL
SELECT i FROM Item i
WHERE i.DTYPE in (’A', 'B’)

 

TREAT

TREAT는 JPA 2.1에 추가된 기능으로 타입 캐스팅과 비슷하다.

상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.

 

예 : Item i를 자식 타입인 Book으로 다룸으로써 author에 접근 가능하다.

//JPQL
select i from Item i where treat(i as Book).author = 'kim'

//SQL
select i.* from Item i
where
i.DTYPE='B' and i.author='kim'

 

기타 정리

  • enum은 = 비교 연산만 지원한다.
  • 임베디드 타입은 비교를 지원하지 않는다.

NULL 정의

  • 조건을 만족하는 데이터가 하나도 없을 때,
  • 알 수 없는 값일 때,
  • Null == Null은 알 수 없는 값이다.
  • Null is Null 은 참이다.

이밖에도 쿼리 파라미터 바인딩에서 값 대신 엔티티를 직접 넣어 줄 수 있다. 이때는 해당 엔티티의 기본 키를 가지고 작동한다.


Named 쿼리 : 정적 쿼리

  • 동적 쿼리 : em.createQuery("select ...") 처럼 JPQL을 문자로 완성해서 직접 넘기는 것, 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.
  • 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용하는 것, Named 쿼리라 부르며 한 번 정의하면 변경할 수 없는 정적 쿼리다.

Named 쿼리를 사용함으로써 애플리케이션 로딩 시점에 문법 체크 및 파싱을 미리 해두기 때문에, 오류 체크나 파싱된 결과를 재사용하는데 성능상 지점이 있다. 또한 변하지 않는 정적 쿼리이기 때문에 조회 성능 최적화에도 도움이 된다.

 

사용 방식은 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 XML 문서에 작성할 수 있다.

 

Named 쿼리를 어노테이션에 정의

이름 그대로 쿼리에 이름을 부여해서 사용한다. 

em.createNamedQuery() 메서드를 사용한다.

또한 name에 엔티티명을 붙여주는데 이는 다른 엔티티에서도 동일한 Named 쿼리명을 사용할 수 있어 관리 상 용이하게 하기 위해서이다.

@Entity
@NamedQuery(
  name = "Member.findByUsername”,
  query="select m from Member m where m.username = :username”)
public class Member {
  ...
}

// 사용
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "손흥민")
    .getResultList();

 

만약 하나의 엔티티에 2개 이상의 Named 쿼리를 작성하려면 @NamedQueries 어노테이션을 사용한다.

@NamedQueries({
  @NamedQuery(
    name = "Member.findByUsername",
    query= "select m from Member m where m.username = :username"),
  @NamedQuery(
    name = "Member.count",
    query= "select count(m) from Member m")
})

 

JPQL에 관련한 내용이지만 거의 대부분은 SQL에 대한 내용과 유사하다고 볼 수 있다.
따라서 SQL 공부를 열심히 하면 JPQL이나 뒤에서 공부할 Criteria 등에서도 유용할 거라 생각한다.

결론 : SQL 공부 파이팅!