[김영한 - JPA 기본] #10 JPQL - 2

JPQL 경로 표현식


  • .(점)을 찍어 객체 그래프를 탐색하는 것
select m.username -- 상태 필드
from Member m
  join m.team t -- 단일 값 연관 필드
  join m.orders o -- 컬렉션 값 연관 필드
where t.name = '팀A'

경로 표현식 용어 정리


  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드
    • (ex: m.username)
  • 연관 필드(association field): 연관관계를 위한 필드
    • 단일 값 연관 필드:
      @ManyToOne, @OneToOne, 대상이 엔티티 (ex: m.team)
    • 컬렉션 값 연관 필드:
      @OneToMany, @ManyToMany, 대상이 컬렉션 (ex: m.orders)

경로 표현식 특징


  • 상태 필드(state field): 경로 탐색의 끝, 탐색이 더이상 불가하다
  • 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색이 더 가능하다
  • 컬렉션 값 연관 경로: 묵시적 내부 조인 발생, 탐색이 더이상 불가
    • FROM 절에서 명시적 조인을 통해 별칭(alias)을 얻으면 별칭을 통해 탐색 가능

상태 필드 경로 탐색


  • JPQL
select m.username, m.age from Member m
  • SQL
select m.username, m.age from Member m

단일 값 연관 경로 탐색


  • JPQL
select o.member from Order o
  • SQL
select m.*
from Orders o
inner join Member m on o.member_id = m.id

명시직 조인, 묵시적 조인


  • 명시적 조인: join 키워드 직접 사용
select m from Member m join m.team t
  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
select m.team from Member m

경로 표현식 예제


select o.member.team from Order o -- 가능
select t.members from Team -- 가능
select t.members.username from Team t -- 불가
select m.username from Team t join t.members m -- 가능

경로 탐색을 사용한 묵시적 조인 시 주의사항


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

실무 조언


  • 가급적 묵시적 조인 대신에 명시적 조인 사용
  • 조인은 SQL 튜닝에 중요 포인트이기 때문에 반드시 직관적으로 잘 나타내자
  • 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

JPQL 페치 조인 (fetch join)

실무에서 정말정말 중요한 !


  • 일반적인 DB SQL 조인이 아님!
    • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • [ LEFT [OUTER] | INNER ] JOIN FETCH 조인 경로

엔티티 페치 조인


  • JPQL
select m from Member m join fetch m.team
  • SQL
SELECT M.*, T.*, FROM MEMBER M
  INNER JOIN TEAM T ON M.TEAM_ID = T.ID
  • 회원을 조회하면서 연관된 팀(T.*)도 함께 조회 (SQL 참고)

컬렉션 페치 조인


  • 일대다 관계, 컬렉션 페치 조인

  • JPQL

select t from Team t join fetch t.members where t.name = '팀A'
  • SQL
SELECT T.*, M.*
FROM TEAM T
  INNER JOIN MEMBER M
    ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A'

문제는 이 경우 모든 가능한 경우가 발생하여 팀 객체가 여러개 보이는 것처럼 선택된다 !

(강의 pdf 그림을 참고하면 바로 알 수 있다)

페치 조인과 DISTINCT


  • SQL의 DISTINCT 는 중복된 결과를 제거하는 명령이었다

  • JPQL의 DISTINCT 는 2가지 기능을 제공

      1. SQL에 DISTINCT 를 추가
      1. 애플리케이션에 엔티티 중복 제거
        • 위 JPA find의 결과로 나온 중복된 팀을 제거할 수 있다

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


  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다
  • 페치 조인은 연관 엔티티를 함께 조회, 즉 즉시 로딩한다
select t
from Team t join t.members m
where t.name = ‘팀A'

분명 조인을 헀음에도 불구하고 .. 연관 엔티티는 무시된다
아래 처럼 !!

SELECT T.*
FROM TEAM T
  INNER JOIN MEMBER M
  ...

페치조인의 특징과 한계


  • 페치 조인 대상에는 별칭을 줄 수 없다

    • 하이버네이트는 가능, 가급적 사용 (X)
  • 둘 이상의 컬렉션은 페치 조인하면 안됀다 !

    • 컬렉션을 페치 조인하면 페이징 API (setFirstResult, setMaxResults)를 사용할 수 없다
      • 앞에서 본 것 처럼 여러 Row가 뻥튀기 된다 ㅠㅠ
    • @XXX ToOne 시리즈는 가능 하다 (연관 엔티티가 컬렉션이 아닌 단일 값의 경우)
      • 뻥튀기 되는 현상이 없기 때문
    • 하이버 네이트는 단지 경고 로그만 남길 뿐, 메모리에서 페이징 처리를 한다
  • 페치 조인을 사용하는 것이 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다

    • 아래와 같은 경우를 글로벌 로딩 전략이라고 한다, 전역적으로 설정이 할당되기 때문
      • 생각해보면 당연하다, repository, service는 도메인 객체를 의존하기 때문에
        도메인 객체 자체를 전역 변수같은 느낌으로 받아들이면 글로벌 로딩 이라는 네이밍이 이해가 간다
    @OneToMany(fetch = FetchType.LAZY)
    
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩

    • 이 중 최적화가 필요한 곳만 페치 조인을 적용한다

@BatchSize

  • Team에서 Member 객체를 조회할 때 (1+N 문제)
    • fetch 조인이 아닌 이 옵션으로 해결 가능하다
@Entity
public class Team { ...

	@BatchSize(size = 10)
	private List<Member> members = new ArrayList<>();
/* load one-to-many jpql.Team.members */ select
    members0_.TEAM_ID as TEAM_ID5_0_1_,
    members0_.id as id1_0_1_,
    members0_.id as id1_0_0_,
    members0_.age as age2_0_0_,
    members0_.name as name3_0_0_,
    members0_.TEAM_ID as TEAM_ID5_0_0_,
    members0_.type as type4_0_0_
from
    Member members0_
where
    members0_.TEAM_ID in (
        ?, ?
    )

글로벌 세팅으로도 가능

  • 실무에서 보통 글로벌로 쓴다고 한다

  • persistence.xml

<property name="hibernate.default_batch_fetch_size" value="100" />

JPQL 다형성 쿼리


  • 조회 대상을 특정 자식으로 한정

    • 예) Item 중에 Book, Movie를 조회해라
  • JPQL

select i from Item i
where type(i) in (Book, Movie)
  • SQL
select i.* from Item i
where i.dtype in ('Book', 'Movie')

JPQL 다형성 쿼리 : TREAT (JPA 2.1)


  • 자바의 타입 캐스팅과 유사
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
  • FROM, WHERE, SELECT (하이버네이트 지원) 사용

  • JPQL
select i from Item i
where treat(i as Book).author = 'kim'
  • SQL
/* select
    i
from
    Item i
where
    treat(i as Book).author = '영한 킴'  */ select
        item0_.ITEM_ID as ITEM_ID2_2_,
        item0_.name as name3_2_,
        item0_.price as price4_2_,
        item0_1_.artist as artist1_0_,
        item0_2_.author as author1_1_,
        item0_2_.isbn as isbn2_1_,
        item0_3_.actor as actor1_3_,
        item0_3_.director as director2_3_,
        item0_.DTYPE as DTYPE1_2_
    from
        Item item0_
    left outer join
        Album item0_1_
            on item0_.ITEM_ID=item0_1_.ITEM_ID
    inner join
        Book item0_2_
            on item0_.ITEM_ID=item0_2_.ITEM_ID
    left outer join
        Movie item0_3_
            on item0_.ITEM_ID=item0_3_.ITEM_ID
    where
        item0_2_.author='영한 킴'

엔티티 직접 사용 - 기본 키 값


JPQL 에서 엔티티를 직접 파라미터로 넣으면 해당 엔티티의 PK를 사용하게 된다

PK 기본 키

  • JPQL
select m from Member m where m = :findMember
  • SQL
select m.* from Member m where m.id = ?

FK 외래 키

  • JPQL
select m from Member m where m.team = :findTeam
  • SQL
select m.* from Member m
  inner join team t
    on m.team_id = t.id
where t.id = ?

JPQL Named 쿼리 - 정적 쿼리


  • 미리 정의해서 이름을 부여하고 사용하는 JPQL

  • 어노테이션, XML에 정의

  • 정적 쿼리

    • 애플리케이션 로딩 시점에 초기화 후 재사용
    • 해당 시점에 검증하기 때문에 문법 오류를 조기에 파악 가능
  • spring data jpa에 더 간단하게 사용 할 수 있는 어노테이션이 있다

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query

JPQL 벌크 연산


  • 쿼리 한 번으로 여러 테이블 로우 변경

여러개를 insert, update, delete를 하고자 하는데 하나의 변경 건하나의 쿼리가 나가는 것은 비용이 비싸다!

여러 변경 건에 대해 처리를 하고자 하는 것 이게 벌크 !

예시 코드


String query = "update Member m set m.age = m.age +1 ";

int resultCount = em.createQuery(query).executeUpdate();  // updated row

out.println("resultCount = " + resultCount);

벌크 연산 주의 !


  • 영속성 컨텍스트를 무시하고 DB에 바로 쿼리

  • 벌크 연산 수행 후 영속성 컨텍스트를 초기화 하자 !

벌크 연산시 문제의 상황


회원 테이블의 나이를 +1 씩 한 후에 다시 조회했더니 이전의 값이 남아 있는 상황 !

Member userA = Member.builder()
  .name("user a")
  .age(20)
  .build();

Member userB = Member.builder()
  .name("user b")
  .age(20)
  .build();

em.persist(userA);
em.persist(userB);

String query = "update Member m set m.age = m.age +1 ";

int resultCount = em.createQuery(query).executeUpdate();

out.println("resultCount = " + resultCount);

Member findUserA = em.find(Member.class, userA.getId());

out.println("findUserA = " + findUserA);
  • 출력 결과
findUserA = Member(id=4028b8817ea3cf87017ea3cf8a2a0000, name=user a, age=20, type=null)

조회 결과가 갱신된 나이인 21살이 아닌 20살이 출력된다

find 앞에서 캐시를 초기화 해주어야 한다 ! clear() !

완강


image

드디어 ㅎㅎ 모두 완강하였다

조원분들 덕분이다..

단순히 강의를 들을 뿐만 아니라 여러 상황들에 대해 의견도 나누고 실험도 해보면서 진행하느라 힘들었던 것 같다.. ㅎㅎ

덕분에 jpa 이론은 한동안 계속 머리 속에 잠재되어 있을 것 같다.. 조원분들에게 감사하다 ㅎ




© 2020.12. by 따라쟁이

Powered by philz