JPA에 대해 - 객체지향 쿼리 언어(JPQL) 중급

쿠우·2023년 1월 1일
0

객체 지향 쿼리언어(JPQL) 중급 문법

경로 표현식

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

경로 표현식 용어 정리

select m.~~ From Member m 일 때

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

경로 표현식 특징

  • 상태 필드(state field) : 경로 탐색의 끝, 탐색X
  • 단일 값 연관 경로 : 묵시적 내부 조인(inner join) 발생 , 탐색O
  • 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색X
    (FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능)
    ex)
select t.members From Team t-->묵시적 조인 탐색 불가 
select m.username From Team t join t.members m --> 명시적 조인과 별칭을 통해 탐색 

묵시적 내부 조인 발생 = 엔티티영역에서는 편해보이지만 테이블영역에서는 조인일어남(운영하기 어렵다.)
실무에서는 무조건 명시적 조인으로 해야 쿼리 튜닝에도 편하다.
ex)
JPQL : select o.member from Order o 이라면
SQL : select m.* from Orders o inner join Member m on o.member_id = m.id

그렇다면 명시적조인과 묵시적 조인의 뜻은?

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

결론적으로 실무 Tip
1. 가급적 묵시적 조인 대신에 명시적 조인 사용
2. 조인은 SQL 튜닝에 중요 포인트
3. 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

페치 조인(fetch join) ★매우 중요★

  • 실무에서 정말정말 중요함

페치 조인(fetch join)이란?

  • SQL 조인 종류가 아닌 JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 명령어 사용
    페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

엔티티 페치 조인

-회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)

  • SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT
  • 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 

페치 조인을 왜 사용할까?


..
..
//회원1, 회원2에는 팀A 회원3에는 팀B에 넣고 회원과 팀을 다 조회하고 싶을 때 
..
..

// 페치조인 없이 부른다면
String jpql = "select m from Member m";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();

for (Member member : members) {

 System.out.println("username = " + member.getUsername() + ", " + "teamName = " + member.getTeam().name());
} // for

=====결과 요약설명=====
Member에 대해 select문 1Team에 대해 다른팀일 경우마다 n번으로 부르게된다. 
username = 회원1, teamname =A (SQL로 DB에서)
username = 회원2, teamname =A(1차 캐시)
username = 회원3, teamname =B (SQL로 DB에서)

위와 같은 방법을 해결하기 위해서는 페치조인 밖에 사용할 방법이 없다.
아래 부분으로 변경하면

String jpql = "select m from Member m join fetch m.team"; 

조인된 결과로 깔끔하게 DB에서 프록시가 아닌 진짜 데이터로 지연로딩 전혀 없이 한번에 가져온다.
왜냐하면, 지연로딩을 해도 페치조인이 우선으로 먹히기 떄문에

컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인
  • 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' 

컬렉션 페치 조인에서 조심해야할 점

  • 결과를 보면 키값이 다른 N개의 조인데이터에 대해서 뻥튀기 되어 N개가 출력 된다.

String jpql = "select t from Team t join fetch t.members "
List<Team> teams = em.createQuery(jpql, Team.class).getResultList();

for(Team team : teams) {
   System.out.println("team = " + team.getName() + "|members=" + team.getMembers().size());
}//for

=====결과=====
team =A|members=2
team =A|members=2
team =B|members=1
  • 조인된 테이블에서는 Member의 ID값이 다르니까 팀 A의 정보에 대해 회원을 2개로 둔다.
  • 객체의 영역인 영속성 컨텍스트에서는 Team_ID가 같으니까 팀A에 하나로 저장된다.
  • 영속성 컨텍스트에 있는 정보를 컬렉션으로 담을 때 팀A에 대한 정보가 위에서 설명한 페치조인의 성질로 인해 DB에서 바로 호출되어 DB에 있는 컬럼 N개대로 증식된다.
  • 결국 DB에서의 조인과 중복을 ORM 객체의 영역에서 이해 할 수가 없으니 생기는 해프닝이다.

(컬렉션 페치 조인 테이블)

페치조인의 중복 문제는 어떻게 해결하는가

페치 조인과 DISTINCT

  • SQL의 DISTINCT는 중복된 결과를 제거하는 명령
    (SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 실패)
    (PK까지 모든 데이터가 같아야 중복으로 확인하고 제거한다.)
    ex)

  • JPQL의 DISTINCT 2가지 기능 제공

  1. SQL에 DISTINCT를 추가 -> DB영역에서 중복된 결과 제거
  2. 애플리케이션에서 엔티티 중복 제거
// DISTINCT가 추가로 애플리케이션에서 중복 제거시도
// 같은 식별자를 가진 Team 엔티티 제거
select distinct t
from Team t join fetch t.members
where t.name = ‘팀A

페치, 일반 조인의 차이는 무엇?

  • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
    (한 가지에 대한 조인만 한다는 뜻)
    일반조인 예시)
// 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
  • JPQL은 결과를 반환할 때 연관관계 고려하지 않고 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
    여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다.

페치 조인 예시)

// 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
  • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(사실상 즉시 로딩)
  • 페치 조인은 객체 그래프를 SQL로 한번에 조회하는 개념( 영속성 컨텍스트 조회 아니라고 이해함 )

Tip. 객체그래프란?
컴퓨터 과학에서 객체 지향 프로그램에서 객체 그룹은 다른 객체에 대한 직접 참조 또는 중간 참조 체인을 통해 서로 간의 관계를 통해 네트워크를 형성합니다. 이러한 객체 그룹을 그래프 이론에서 연구한 같은 이름의 수학적 객체를 따서 객체 그래프라고 합니다.

페치조인의 한계

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

  • 연관된 애를 다 끌고 왔는데 그 중 한명만 불러오면 4명이 누락되어 이상하게 동작할 수 있음
  • 가급적 사용하지말라(어찌저찌 풀어도 운영에서 어렵다.)
  • 객체그래프를 탐색한다는 것은 해당 객체를 다 찾는데에서 설계 되어있다. 이상한 옵션이 추가되어서 동일한 엔티티를 조회했는데 한 쪽은 5개 한 쪽은 100개면 영속성 컨텍스트 입장에서는 이런 컨트롤에 대해서 보장하지 않는다.
  • 특정 데이터가 필요하면 차라리 그 엔티티에서 조회하라

둘 이상의 컬렉션은 페치 조인 할 수 없다.

  • 데이터 정합성이 안맞을 수 있다.
  • 일대 다대 다가 되어서 곱하기 뻥튀기가 더 심해질 것이다.
  • 페치조인의 컬렉션은 딱 하나만 지정할 수 있다라고 기억하라

컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

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

  • 이유는 아까 봤던 데이터 뻥튀기 즉 DB영역에서의 데이터 중복과 영속성컨텍스트에서의 뜻이 달라서이다.
    페이징 하게 되면 기존에 있던 테이블에 대해서 사이즈에 맞춰 자르게 되고 영속성 컨텍스트 입장에서는 엔티티와 테이블을 매핑 했을 때 자른 만큼 정보가 누락된다.

    잘풀어서 설명 예시)
    팀A에 ID값이 1,2인 회원을 페치 조인 하면
    조인 테이블에 회원의 ID 값이 달라 2개의 정보가 들어간다.
    여기서 사이즈를 1로 하게 되면 회원2의 정보는 잘리고
    영속성 컨텍스트에서는 팀A에 대한 정보를 회원 1만 저장한다.
    그럼 애플리케이션 영역에서 객체 그래프하고 정보가 안맞는다.

  • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)

페치 조인의 특징

연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화 (N+1문제의 해결)

  • 아까 처음 페치 조인을 왜 사용할까? 목차에서 설명했었음
  • @BatchSize라는 어노테이션에 사이즈를 주면 사이즈 수만큼 SQL의 IN절을 사용해서 조회 테이블에서 데이터를 가져온다. 페치 조인에서도 컬렉션일 때 해결이 안되어서 그 때 사용
    (https://joont92.github.io/jpa/JPA-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94/ << 참고하기 좋은 블로그)
  • @BatchSize 을 보통 글로벌 세팅으로 깔고간다.

엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함

  • @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳(N+1문제가 터지는 곳)은 페치 조인 적용

페치조인 마지막 정리

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

Tip. 조회의 방법 대표적 3가지
1. 엔티티를 패치조인을 사용하여 조회해서 그대로 사용한다.
2. 패치조인을 한 다음 애플리케이션에서 DTO로 바꿔 전달한다.
3. 처음 JPQL 짤 때부터 new operation DTO로 스위칭 하여 가져온다.

다양한 쿼리문법

다형성 쿼리

조회 대상을 특정 자식으로 한정 type(~~)

  • 예) Item 중에 Book, Movie를 조회해라
// 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)

  • 자바의 타입 캐스팅과 유사
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
  • FROM, WHERE, SELECT(하이버네이트 지원) 사용
  • ex) 부모인 Item과 자식 Book(기본키 저자)이 있다
// JPQL
select i from Item i
where treat(i as Book).auther = ‘koo’ 
--SQL
select i from i
where i.DTYPE = ‘B’ and i.auther = ‘koo’

JPQL 엔티티 직접 사용

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

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
    ex)
// 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
  • 엔티티를 파라미터로 전달
String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql)
                              .setParameter("member", member)
                              .getResultList(); 
  • 식별자를 직접 전달
String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql)
                              .setParameter("member", memberId)
                              .getResultList(); 
  • 실행된 SQL은 똑같다.
select m.* from Member m where m.id=? 

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

  • 다른 엔터티를 가지고 써도 등록된 외래키를 식별한다 .
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name= "TEAM_ID")
private Team team

외래키가 아닌 엔티티 사용 할 때

Team team = em.find(Team.class, 1L);

String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
                               .setParameter("team", team)
                               .getResultList(); 

외래키를 통해 식별할 때

String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
                               .setParameter("teamId", teamId)
                               .getResultList(); 

쿼리문의 결과는 같다.

select m.* from Member m where m.team_id=?

Named 쿼리 - 정적 쿼리

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

어노테이션, XML에 정의해놓고 사용 (마치 MyBatis 같지)

  • [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>
  • 어노테이션(결국 나중에 스프링JPA랑 섞어서 간단하게 사용한다. )
@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","회원1")
            .getResultList();

애플리케이션 로딩 시점에 미리 초기화 후 재사용

애플리케이션 로딩 시점에 미리 쿼리를 검증 (이게 핵심으로 중요하다)

  • 잘못 적었으면 실행시점부터 어디가 잘못된건지 캐치하기 쉬움

Named 쿼리 환경에 따른 설정

  • XML이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.

JPQL - 벌크 연산

벌크 연산이란?

  • 여러 건(대량의 데이터)을 한 번에 수정하거나 삭제하는 방법
  • JPA는 실시간성인 성격에 최적화 되어있지만 이런 기능도 제공한다.

벌크연산 필요 상황

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

벌크 연산의 사용 (executeUpdate())

  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DELETE 지원
  • INSERT(insert into .. select, 하이버네이트 지원)

String qlString = "update Product p " +
                            "set p.price = p.price * 1.1 " +
 "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(qlString)
                                 .setParameter("stockAmount", 10)
                                 .executeUpdate(); 

벌크 연산의 주의

  • 문제점 : 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
  • 사용법1 : 영속성 컨텍스트에 아무것도 없으면 아무런 작업도 하지말고 벌크 연산을 먼저 실행하라
  • 사용법2 : 영속성 컨텍스트에 뭐가 있으면 벌크 연산 수행 후 영속성 컨텍스트 초기화 후 다시 엔티티 가져오기(중요)
    예시 설명) 예전에 조회한 멤버가 있으면 영속성 영역에 연봉이 5000만이었다. 벌크연산이 날라가면서 6000만이 되었다. 그럼 DB에는 6000만원인데 애플리케이션에는 5000만원으로 남아있다. 따라서 영속성컨텍스트를 싹 초기화 시키면 나중에 DB에서 영속성컨테스트로 다시 담아와 6000만원으로 조회가 된다.
profile
일단 흐자

0개의 댓글