Spring Framework/[인프런] Java ORM 표준 프로그래밍 - JPA

Chapter 11. 객체지향 쿼리 언어2 - 중급 문법

계란💕 2022. 9. 9. 12:41

11.1 경로 표현식

 

경로 표현식

sql
닫기
select m.username -> 상태 필드
from Member m 
join m.team t -> 단일 값 연관 필드
join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'
  • 점을 찍어서 객체 그래프를 탐색할 수 있는 식을 경로 표현식이라고 한다.
  • 상태 필드(state field): 단순히 값을 저장하기 위한 필드, 경로 탐색의 끝이며 더이상 탐색 X (불가)
  • 연관 필드(association field): 연관 관계를 위한 필드
    1. 단일 값 연관 경로
      1. 대상이 엔티티인 경우를 말한다.   ex) @ManyToOne, @OneToOne
      2. 묵시적 내부 조인 발생(join키워드 직접 사용), 탐색 O
    2. 컬렉션 값 연관 경로
      • 대상이 컬렉션인 경우,   ex) @ManyToMany, @OneToMany
      • 묵시적 내부 조인 발생, 탐색 X
      • FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
  • 실무에서 명시적 조인을(JOIN 키워드 직접 사용) 써야 덜 헷갈리고  에러가 발생하지 않는다.

 

 

  Ex) 단일 값 연관 경로

java
열기

  Note) 실행 결과

  • 묵시적 내부 조인이 발생
  • 실무에서 이렇게 발생하도록 짜지 않는다.
java
열기

 

 

  Ex) 컬렉션 값 연관 경로

java
열기

  Note) 실행 결과

  • 값이 없어서 출력은 안 된다.
  • t.members 뒤에 점을 찍으면 뭔가 추천되는 값이 없다. 컬렉션 자체를 가리키기 때문이다.
java
열기

 

 

  Ex)  상태 필드 - inner join 없음

java
열기

  Note) 실행 결과

java
열기

 

 

  Ex) 컬렉션 값 연관 경로

  • 그런데  t.members 뒤에는 더이상 탐색이 안된다.
  • 따라서, 명시적 조인을 써야한다. 별칭을 설정하면 별칭을 통해 탐색 가능하기 때문이다.
    • 권장: SELECT m FROM Team t JOIN t.members m
java
열기

  Note) 실행 결과

java
열기

 

 

명시작 조인, 묵시적 조인

  • 명시적 조인: "JOIN" 키워드를 직접 사용 
  • 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생한다. (내부 조인만 가능)
    • 외부 조인하려면 명시적 조인을 해야한다.

 

 

  Ex) 경로 표현식

  • 컬렉션은 자기 자신 또는 size() 까지만 가져올 수 있고 더 이상 탐색은 불가능하다.
sql
닫기
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 튜닝에 중요한 포인트

 

 

 

11.2 페치 조인(fetch join) (1) - 기본

 

fetch join이란?

  • 지연로딩으로 세팅해도 즉시 로딩(EAGER)로 가져오는 쿼리이다. (지연 로딩보다 페치 조인이 우선) 
  • 페치 조인은 SQL 조인의 종류가 아니다.
  • JPQL에서 성능 최적화를 위해 제공한다.
  • 연관된 엔티티나 컬렉션을 SQL로 한번에 조회하는 기능이다.

 

 

엔티티 페치 조인

  • 회원을 조회하면서 연관된 팀도 함께 조회한다. SQL 한방 쿼리
  • SQL을 보면 회원 뿐만 아니라 팀(T.*) 도 함께 SELECT

 

  Ex) 엔티티 페치 조인 - JPQL에 따라 실제 실행되는 SQL

sql
닫기
[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

 

 

FK => PK  (MEMBER.TEAM_ID -> TEAM.ID)

 

 

 

결과

 

 

  Ex) 페치 조인이 나오게 된 이유

java
열기

  Note) 실행 결과

  • LAZY 설정되어 있어서 프록시로 들어오고 getName() 호출한 시점에야 데이터베이스에 쿼리를 날린다.
  • 쿼리가 너무 많이 나온다. "N + 1" 문제
    • LAZY, EAGER 둘다  "N + 1" 문제가 발생한다
    • fetch join으로 해결 가능
java
열기

 

 

  Ex) fetch join - ManyToOne

java
열기

  Note) 실행 결과

  • join으로 한방 쿼리가 나간다. 멤버와 팀 inner join
  • for문에서 getTeam()으로 얻는 team은 프록시가 아니다.
    • createQuery() 실행되는 시점에 얻은 팀은 fetch join으로 얻은 실제 엔티티(프록시 아님)
    • 지연로딩 없이 깔끔하게 나온다.
java
열기

 

 

 

컬렉션 페치 조인

  • 일대다 관계
java
닫기
[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'

 

 

 

  Ex)  컬렉션 페치 조인 시 주의사항

java
열기

  Note) 실행 결과

java
열기
  • 팀A 내용이 왜 2개가 출력될까?
  • 데이터베이스 특성 때문에 일대다 조인은 여러 개 쿼리가 실행될 수 있다.
  • 팀, 멤버 테이블을 조인하는데 두 줄이 나온다. (멤버의 pk가 다르니까)
  • JPA는 이 시점에 개입 불가능하다. 

 

 

  Ex) FETCH JOIN이 있는 경우와 없는 경우의 result size 차이

  • JOIN FETCH 없을 때 => result size: 2
java
열기

 

  • JOIN FETCH 있을 때 => result size = 3
java
열기

  Note) 실행 결과

  • 위 결과가 왜 다를까?
  • 조인하면서 데이터가 커지기 때문이다.
  • 일대다 조인하면 데이터가 더 커질 수 있다는 특성 때문에 이런 현상이 생긴다.

 

 

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령이다.
  • JPQL의 DISTINCT는 2가지 기능 제공
    1. SQL에 DISTINCT 추가
    2. 애플리케이션에서 엔티티의 중복을 제거한다.
  • DISTINCT가 추가로 애플리케이션에서 중복 제거를 시도한다.
  • 같은 식별자를 가진 Team 엔티티를 제거한다.

 

 

  Ex) DISTINCT

  • SQL에 DISTINCT를 추가하지만 데이터가 다르기 때문에 SQL 결과에서 중복 제거 실패한다.
java
닫기
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A’

 

 

  Ex) DISTINCT

java
열기

  Note) 실행 결과

  • 컬렉션에서 중복을 제거해준다. => size = 2
java
열기

 

 

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

  • 일반 조인 실행 시 연관된 엔티티를 함께 조회하지 않는다. 
  • JPQL은 결과를 반환할 때, 연관 관계를 고려하지 않는다.
    • 단지, SELECT 절에 지정한 엔티티만 조회할 뿐이다.
    • 아래 예제에서 팀 엔티티만 조회하고 회원 엔티티는 조회하지 않는다.
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다. (즉시 로딩)
    • 페치 조인을 사용하면 즉시 로딩이 일어난다고 보면 된다.
  • 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다. 한 방 쿼리
java
닫기
[JPQL]
select t
from Team t join t.members m
where t.name = ‘팀A'

[SQL]
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID 
WHERE T.NAME = '팀A'

 

 

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

  • 일반 조인
java
열기

  Note) 실행 결과 - 일반 조인

  • 조인했는데 SELECT절에서 팀 id랑 name만 나온다. (윗 부분 JOIN)
  • 데이터 늘어나서 3으로 출력된다.
  • for문 돌릴 때, members가  초기화되지 않는 상태이다.
  • 컬렉션이 프록시는 아니지만 데이터가 없어서 쿼리가 쭉 나온다. (맨 아래)
java
열기

 

  • fetch join 추가
java
열기

 

  Note) 실행 결과 - fetch join

  • 일반 조인과 다르게 맨 처음 SELECT 절에서 member.id, name 뿐만 아니라 멤버에 대한 내용도 모두 가져온다.
  • 나중에 for문을 돌려서  getName(), getMember()를 출력하면 한 번에 멤버 정보를 가져온다.
java
열기

 

 

 

11.3  페치 조인 2 - 한계

 

페치 조인 특징과 한계

  • 페치 조인 대상에는 원칙적으로 별칭을 줄 수 없다.
    • 하이버네이트는 가능
    • 관례적으로 별칭을 쓰지 않도록 한다. (페치 조인을 여러 단계로 하는 경우는 가끔 별칭을 쓰기도 한다.)
    • 가급적 사용 X
    • 페치 조인은 연관된 것들을 모두 가져오는 기능이다.
    • ex) 팀 안에 5명이 있는데 그 중 3 명만 조회하는 경우?  => 위험하다. (지워지거나 오작동 가능성) 
      • 사상이 안 맞는 "정합성(무모순성, Data Consistency)" 이슈 때문
      • 이런 경우는 처음부터 5명 조회하는 SELECT 5명 쿼리를 날려야한다.
  • 둘 이상의 컬렉션을 페치 조인할 수 없다.
    •  이 부분도 데이터 정합성 이슈가 있다.
    • ex) Team의 orders? 
    • 일대다도 데이터 뻥튀기 되는데 이 경우는 일대다"대"다 => 데이터가 예상치 못하게 뻥튀기 가능하다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults) 사용 불가
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 (데이터 뻥튀기가 안 되서 가능)
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험)
    • 페이징은 철저히 데이터베이스 중심적이다. rows를 줄이려는 기능
  • 연관된 엔티티들은 SQL 한 번으로 조회된다. => 성능 최적화
  • 페치 조인이 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다.
    • @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩이다.
    • 최적화 필요한 곳에만  페치 조인 적용하면 대부분 성능 문제가 해결된다.

 

페치 조인 정리

  • 모든 걸을 페치 조인으로 해결할 수는 없다.
  • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
  • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야하면 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터만 조회해서 DTO로 반환하는 것이 효과적이다.

 

 

  Ex) 컬렉션 페치조인과 페이징 API

java
열기

 

  Note) 실행 결과

  • DB에서 팀에 대한 데이터를 다 끌고 온다. 장애 나기 좋은 조건이다.
java
열기
  • 경고 - 페치조인과 페이징
java
닫기
WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

 

 

 

  Ex) 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 

 

  • 기존의 쿼리
String query = "SELECT t FROM Team t join fetch t.members";

 

  • 바뀐 쿼리 - 멤버부터 시작
String query = "SELECT m FROM Member m join fetch m.team t";

 

  Note) 실행 결과 - 회원 => 팀 다대일은 페이징에 문제가 없다.

 

 

 

  Ex) BatchSize를 쓰게 된 배경

  • BatchSize는 하위 엔티티를 로딩할 때, 한번에 상위 엔티티 ID를 지정한 숫자만큼 in Query로 로딩한다.
java
열기

  Note) 실행 결과

  • lazy 로딩이 2번 일어난다. 성능 안 좋다.
  • 팀을 10개 조회하면 연관된 멤버를 조회하는데 추가적으로 쿼리가 10번 이상 나갈 것이다.
    • 해결 방법: @BatchSize()
java
열기

 

 

  Ex) @BatchSize

  • 팀 클래스
java
열기
  • Main 클래스
java
열기

  Note) 실행 결과

  • 사이즈는 2개로 정상적으로 나온다.
  • 그런데 그 아래 쿼리를 보면?
    • 아래의 select에서 멤버를 가져오는데 in Query 안에 "?, ?"라고 나온다. TeamA, TeamB와 연관된 내용을 모두 가져온 것이다.
      • (기존에 @BatchSize를 쓰기 전에는 @  where members0_.TEAM_ID=? 이었다.)
    • 팀에서 멤버스를 가져올 때, LAZY 로딩 상태이다.
    • 매인 클래스에서 List<>에 담긴 Team을 in Query로 100 개씩 넘긴다.
    • 쿼리 하나 + lazy 로딩 쿼리 n개 => n + 1 문제
      • 페치 조인 또는 batchSize() 로 해결한다.
java
열기

 

  • @BatchSize 대신에  properties에 추가하더라도 같은 기능을 사용 가능
    • 쿼리가 n + 1이 아니라 테이블 수를 맞출 수 있어서 최적화 가능하다.
<property name="hibernate.default_batch_fetch_size" value="100" />

  Note) 실행 결과 - 결과는 동일하다.

 

 

 

11.4 다형성 쿼리

 

TYPE

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

  Ex) Item 중에 Book(B), Movie(M)를 조회

java
닫기
[JPQL]
select i from Item i
where type(i) IN (Book, Movie) 

[SQL]
select i from i
where i.DTYPE in (‘B’, ‘M’)

 

 

TREAT (JPA 2.1)

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

 

 

  Ex) TREAT 

  • 부모 Item, 자식 Book 
  • 다운 캐스팅 처럼 사용
java
닫기
[JPQL]
select i from Item i
where treat(i as Book).auther = ‘kim’ 

[SQL]
select i.* from Item i
where i.DTYPE = ‘B’ and i.auther = ‘kim’

 

 

 

11.5 JPQL 엔티티 직접 사용

 

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

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
    • COUNT() 안에 멤버의 기본 키를 직접 넣어 버린다.
    • 비교하기 위한 값은 PK니까 ID가 매개변수로 들어간다.
java
닫기
[JPQL]
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용 

[SQL](JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m

 

 

  Ex) 엔티티를 파라미터로 전달하나 식별자를 전달하나 결과는 동일

java
열기

 

 

엔티티 직접 사용 - 외래 키 값

 

  Ex)

  •  아래에서 where절에 m.team: 외래 키, 파라미터 team은 기본 키
java
열기

 

 

 

11.6 Named 쿼리

 

Named 쿼리 - 정적 쿼리

  • 엔티티에 이름이 부여된 쿼리를 미리 등록해두고 사용하는 기능을 말한다. 쿼리를 재활용한다.
  • 정적 쿼리만 가능하다.
  • 애너테이션이나 XML에 Named 쿼리를 정의한다.
  • 애플리케이션 로딩 시점에 초기화 후, 재사용한다.
    • 정적 쿼리는 변하지 않는다. 따라서, 로딩 시점에 쿼리를  SQL로 파싱하고 캐시한다. 
    • JPQL는 기본적으로 SQL로 변환한 다음에 실행되는데 네임드 쿼리를 로딩 시점에 딱 한 번 실행되고 캐시되니까 비용이 거의 없다고 봐도된다.
  • 애플리케이션 로딩 시점에 쿼리를 검증한다.

 

 

  Ex) Named 쿼리 - 애너테이션

  • Member 클래스
    • 여기에 쿼리를 잘못 입력하면 SQLsyntax 에러가 난다.
    • 클래스 이름을 앞에 붙여주는 게 관례
java
열기
  • Main
    • username을 "회원1"로 수정한다.
java
열기

 

  Note) 실행 결과

  • 회원1번 데이터를 가져온다.
  • 쿼리가 실행되서 이름이 바뀐 채로 출력된다.
java
열기

 

 

 

Named 쿼리  - XML에 정의

  • persistence에 ormMember라는 mapping파일에 넣는다.
  • 아래와 같이  데이터를 넣어주면 네임드 쿼리를 불러올 때, 애너테이션에서 불러올 수도 있고 ormMember라는 xml 파일에서도 불러올 수 있다.
  • xml이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포 가능
java
닫기
[META-INF/persistence.xml]
<persistence-unit name="jpabook" >
 <mapping-file>META-INF/ormMember.xml</mapping-file>


[META-INF/ormMember.xml]
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
     <named-query name="Member.findByUsername">
         <query><![CDATA[
             select m
             from Member m
             where m.username = :username
         ]]></query>
     </named-query>
 <named-query name="Member.count">
 <query>select count(m) from Member m</query>
 </named-query>
</entity-mappings>

 

 

cf) Named 쿼리 - JpaRepository

  • 스프링 jpa를 쓰면 JpaRepository를 상속 받은 인터페이스의 메서드 위에 @Query()를 선언하고 괄호 안에 Named 쿼리를 쓸 수 있다.
  • jpa가 이를 네임드 쿼리로 등록한다. 애플리케이션 로딩 시점에 파싱도 하고 오류도 잡아 낸다. - 큰 장점
  • 위의 예제대로 쓰는 것보다 이 방법이 더 좋다.

 

 

11.7 벌크(bulk) 연산

 

벌크 연산

  • ex) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
  • JPA 변경 감지 기능으로 실행하려면 SQL이 너무 많이 실행된다.
    1. 재고가 10개 미만인 상품을 리스트로 조회
    2. 상품 엔티티의 가격을 10% 증가한다.
    3. 트랜잭션 커밋 시점에 변경 감지가 동작한다.
  • 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다.

 

 

벌크 연산 예제

  • 쿼리 한 번으로 여러 테이블(엔티티)의 로우를 일괄적으로 변경한다. (엔티티)
  • executeUpdate(): 쿼리를 실행하고 나서 영향 받은 엔티티의 수를 반환한다.
  • UPDATE, DELETE 지원한다.
  • INSERT(하이버네이트는 insert into ... SELECT 지원)

 

 

벌크 연산 주의 사항

  • 벌크 연산은 영속성 컨텍스에 저장하는 과정을 생략하고 데이터베이스에 직접 쿼리한다.
    • 데이터 정합성에 어긋날 수 있다.
    • 아래 두 가지 중 선택해서 쓴다.
    • 벌크 연산을 먼저 실행한다.
    • 벌크 연산 수행 후, 영속성 컨텍스트를 초기화한다. em.clear().

 

 

  Ex) 벌크 연산

  • 초기화를 안 화면? 
java
열기

 

  Note) 실행 결과

  • createQuery()에서 설정한대로 나이가 20살이 들어가지 않는다.
    • 초기화 em.clear()를 안했기 때문이다.
    • 초기화하면 영속성 컨텍스트의 내용이 모두 지워져서 그 다음에 em에서 조회하는 경우, 데이터베이스의 정보를 가져온다.
java
열기

 

  • 올바른 벌크 연산
java
열기

  Note) 실행 결과 - findMember = 20

 

 

 

 

출처 - https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

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

www.inflearn.com