Java

[JPA(Jakarta Persistence API)] JPQL 문법(경로 표현식, 페치 조인, 다형성 쿼리, 엔티티 직접 사용, Named 쿼리, 벌크연산)

ride-dev 2024. 3. 15. 23:10

경로 표현식

JPQL에서 경로 표현식을 통해 ( . 점으로) 객체 그래프를 탐색할 수 있습니다.

select m.name		// 상태 필드로 그래프를 탐색
from Member m
join m.team t		// 단일 값 연관 필드 (단방향 연관관계)
join m.orders o		// 컬렉션 값 연관 필드 (orders 가 컬렉션인 경우)
where t.name = 'A'

상태필드

상태 필드(state field)는 단순히 값을 저장하기 위한 필드이며, 더 이상 탐색할 수 없습니다.

//JPQL
select m.name, m.age from Member m

//SQL
select m.name, m.age from Member m

연관필드

연관 필드(association field)는 연관관계를 위한 필드입니다.

단일 값 연관 필드

@ManyToOne, @OneToMany, 대상이 엔티티이며, 묵시적 내부 조인이 발생합니다. 엔티티 내부를 더 탐색할 수 있습니다.

//JPQL
select o.member
from Order o

//SQL
select m.*
from Orders o
inner join Member m on o.member_id = m.id

컬렉션 값 연관 필드

OneToMany, @ManyToMany, 대상이 컬렉션이며, 묵시적 내부 조인이 발생하고, FROM절의 명시적 조인을 통해 얻은 AS로 탐색할 수 있습니다.

묵시적 내부 조인이 발생하면 항상 내부 조인을 하며 추적이 복잡해질 수 있습니다.

또한 조인을 명시적으로 사용해야 최적화가 용이하기 때문에,

join 키워드를 직접 사용하여 명시적 조인을 진행합니다.

// 묵시적 조인
select m.team from Member m

// 명시적 조인
select m from Member m join m.team t

페치조인 FETCH JOIN

fetch join은 JPQL에서 성능 최적화를 위한 것으로 SQL 조인 종류에 속하지 않습니다.

fetch join을 통해 연관된 엔티티나 컬렉션을 하나의 쿼리로 조회할 수 있습니다.

 

::= [LEFT [OUTER] | INNER] JOIN FETCH 조인경로

연관관계가 존재하지만, 다른 테이블의 데이터를 사용하지 않는 경우, 지연 로딩을 사용하여 N+1가 발생하지 않습니다.

조인을 사용하여 DB데이터를 불러오고 사용할 경우,

지연 로딩과 즉시 로딩 모두 N+1문제가 발생합니다.

 

일반 조인 시 연관된 엔티티를 함께 조회하지 않습니다.

JPQL은 결과 반환 시 연관관계를 고려하지 않고 SELECT 절에 지정한 엔티티만 조회합니다.

페치 조인을 사용할 때만 연관된 엔티티를 함께 조회합니다(즉시 로딩).

페치 조인은 객체 그래프를 SQL 한 번에 조회합니다.

 

엔티티 fetch join, N:1 다대일 관계

하나의 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

String jpql = "select m from Member m join fetch m.team";
List<Memnber> members = em.createQuery(jpql, Member.class).getResultList();

컬렉션 fetch join, 1:N 일대다 관계

//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'

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

이렇게 조회를 했을 때, 다에 해당하는 데이터의 수에 따라 컬렉션의 크기가 증축됩니다.

(DB에서 반환한 ROW만큼 컬렉션을 만듭니다)

팀이 하나, 멤버가 둘이면 JOIN 시 반환되는 테이블 ROW 또한 2이기 때문에,

2개의 Team을 반환하되, 각 Team에 속해 있는 데이터는 2개 입니다.

컬렉션 DISTINCT

앞서 나온 컬렉션 fetch join의 데이터 중복을 방지하려면 distinct를 사용합니다.

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

SQL에서 반환된 데이터는 중복된 데이터가 아니기 때문에 SQL 결과에는 적용되지 않지만,

distinct는 추가로 애플리케이션에서 같은 식별자를 가진 엔티티를 제거하도록 동작합니다.

페치 조인의 특징과 한계

페치 조인 대상에는 별칭(as)을 줄 수 없습니다.

(하이버네이트는 가능하지만, 영속성 컨텍스트에 문제가 생길 수 있기에 가급적 사용을 지양하는 것이 관례입니다)

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

데이터가 많이 생성될 수 있습니다.

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

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

하이버네이트는 메모리에서 페이징을 시도하므로 많은 리소스를 사용하게 되므로, 사용하지 않도록 합니다.

 

application.properties에서 배치 사이즈를 설정하여 N+1 조회에서 조회할 데이터 수를 지정하여 fetch join을 최적화할 수도 있습니다.

 

페치 조인을 통해 연관관계의 엔티티를 SQL 한 번으로 조회할 수 있습니다.

기본적으로 지연로딩을 적용하고, 필요시 즉시 로딩을 사용합니다.

최적화가 필요하다면 페치 조인을 적용합니다.

 

모든 것을 페치 조인으로 해결할 수는 없지만,

객체 그래프를 유지할 때 사용하면 효과적입니다.

여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적입니다.

다형성 쿼리

다형적으로 설계를 했을 때,

특정 자식에 대해 조회할 수 있습니다.

TYPE

//JPQL
select b from baseEntity b
where type(b) IN (Mebmer, Seller)

//SQL
select b from b
where b.DTYPE in ('M','S')

TREAT

//JPQL
select b from baseEntity b
where treat(b as Member).email = 'ride@tistory.com'

//SQL
select b.* from BaseEntity b
where b.DTYPE = 'M' and b.email = 'ride@tistory.com'

엔티티 직접 사용

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용합니다.

// JPQL
// 엔티티의 아이디를 사용
select count(m.id) from Member m
// 엔티티를 직접 사용
select count(m) from Member m

// SQL
select count(m.id) as cnt from Member m
// 기본 키 값
// 엔티티를 파라미터로 전달
String jpql = "select m from Member m where m = :membner";
List resultList = em.createQuery(jpql)
                    .setParameter("member", member)
                    .getResultList();
                    
// 식별자를 직접 전달
String jpql = "select m from Member m where m.id = :memberId";
List resultList = em.creasteQuery(jpql)
                    .setParameter("memberId", memberId)
                    ,getResultList();

// SQL
select m.* from Member m where m.id=?
// 외래 키 값
Team team = em.dinf(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();
                    
// SQL
select m.* from Member m where m.team_id=?

객체 입장에서는 기본 키와 외래 키 둘 다 식별자와 매칭이 됩니다.

Named  쿼리

Named 쿼리는 미리 정의하여 이름을 부여해두고 사용하는 JPQL입니다.

Named 쿼리는 정적쿼리입니다.

Named 쿼리는 어노테이션,  XML에 정의합니다.

 

애플리케이션 로딩 시점에 초기화 후 재사용합니다.

애플리케이션 로딩 시점에 쿼리를 검증합니다.

따라서 문법 오류가 있으면 컴파일 오류를 통해 개발자가 오류를 확인할 수 있습니다.

@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", "user1")
                            .getResultList();

(Spring Data JPA를 사용하면, Repository에서 @Query를 통해 등록할 수 있습니다)

벌크연산

JPA의 변경 감지 기능을 통해 대용량 데이터를 수정하려면 리소스가 낭비됩니다.

(조회 -> 수정 -> 트랜잭션 커밋 시점에 변경 감지, 변경 데이터 수만큼 쿼리 발생)

벌크 연산을 통해 쿼리 한 번으로 여러 테이블(엔티티) row를 변경할 수 있습니다.

(executeUpdate() 는 변경된 엔티티 수를 반환하는 메서드입니다)

UPDATE, DELETE 를 지원합니다.

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

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

em.clear(); // 영속성 컨텍스트 초기화

벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리를 동작시키는 것이므로,

영속성 관리에 대한 주의가 필요합니다.

따라서 벌크 연산을 먼저 실행하거나,

벌크 연상 수행 후 영속성 컨텍스트를 초기화합니다.

 

(Spring Data JPA 에서는 @Modifying를 사용합니다.)

728x90