티스토리 뷰

ORM/JPA

[JPA] 8. JPQL(2)

mandykr 2022. 4. 14. 16:38

목차

1. 경로 표현식

2. 페치 조인

3. 다형성 쿼리

4. 엔티티 직접 사용

5. Named 쿼리

6. 벌크 연산

 

 

 

 

1. 경로 표현식

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

select m.username -> 상태 필드
 from Member m
 join m.team t -> 단일 값 연관 필드
 join m.orders o -> 컬렉션 값 연관 필드
where t.name = '팀A'

 

상태 필드(state field)

단순히 값을 저장하기 위한 필드(ex: m.username)

select m.name, m.age from Member m

 

연관 필드(association field)

연관관계를 위한 필드

  • 단일 값 연관 필드: @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
  • 컬렉션 값 연관 필드: @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

 

단일 값 연관 경로

묵시적 내부 조인 발생, team.name 탐색 O

select m.team.name from Member m

 

 

컬렉션 값 연관 경로

묵시적 내부 조인 발생, t.members.name과 같이 탐색 X

select t.members from Team t

FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통 해 탐색 가능하다.

select m.name from Team t join t.members m

 

묵시적 조인 주의사항

가급적 묵시적 조인 대신 명시적 조인 사용

조인은 SQL 튜닝에 중요 포인트

묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움

JPQL과 SQL의 형태를 최대한 맞춰 명시적 조인을 사용하자.

 


 

2. 페치 조인(fetch join)

SQL 조인의 종류가 아니며 JPQL에서 성능 최적화를 위해 제공하는 기능

연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회

join fetch 명령어 사용

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

for (Member member : resultList) {
    System.out.println("username = " + member.getName() + "," +
            " teamName = " + member.getTeam().getName());
}

 

일반 조인

( select 절에서 m.team을 조회하지 않는 경우 )

select m from Member m join m.team

조회하는 시점에 Team의 프록시가 영속성 컨텍스트에 담기고

즉시 로딩인 경우 Member를 조회한 후 Team을 조회하는 쿼리가 실행되고

지연 로딩인 경우 실제 Team의 데이터를 사용하는 시점에 조회 쿼리가 실행된다.

 

[즉시 로딩]

 

 

 

 

[지연 로딩]

 

 

회원의 소속 팀이 모두 다른 경우 조회되는 Member 수 만큼 Team을 조회하는 쿼리가 실행되기 때문에

지연로딩, 즉시로딩 모두 N + 1 문제가 발생한다.

 

페치 조인

페치 조인은 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선하다.

( @OneToMany(fetch = FetchType.LAZY) // 글로벌 로딩 전략 )

select m from Member m join fetch m.team

조회하는 시점에 Team의 프록시가 아닌 실제 엔티티가 영속성 컨텍스트에 담긴다.

 

컬렉션 페치 조인

Team과 Member를 DB에서 일대다 조인하면 Member의 수 만큼 Team이 중복되어 조회된다.

팀A에 회원1과 회원2가 소속되어 있을 때 팀A에 해당하는 데이터가 2개의 row로 조회된다.

[SQL]
SELECT M.*, T.* FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T.ID

 

JPQL도 마찬가지로 같은 객체이지만 팀A의 참조값이 페치 조인 결과 리스트에 중복되어 저장된다.

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

for(Team team : resultList) {
    System.out.println("teamname = " + team.getName() + ", team = " + team);
    for (Member member : team.getMembers()) {
        System.out.println("-> username = " + member.getName()+ ", member = " + member);
    }
}

 

페치 조인과 DISTINCT

SQL 상에서 DISTINCT는 서로 다른 row이기 때문에 중복이 제거되지 않고

JPQL 상에서 DISTINCT는 JPA가 중복되는(같은 식별자를 가진) 엔티티를 제거해준다.

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

 

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

JPQL은 결과를 반환할 때 연관관계를 고려하지 않고 SELECT 절에 지정한 엔티티만 조회할 뿐이다.

페치 조인은 연관된 엔티티도 함께 조회한다. 객체 그래프를 SQL 한번에 조회하는 개념.

( N + 1 문제를 페치 조인으로 해결 )

 

 

페치 조인의 한계

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

select t from Team t join fetch t.members m where m.age > 10

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

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

일대다는 데이터 중복으로 row 수가 증가하기 때문에 페이징할 수 없다.

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

다대일, 일대일 조인으로 변경해서 해결해야 한다.

 

페치 조인의 특징

  • 연관된 엔티티들을 SQL 한 번으로 조회
  • N + 1 문제를 해결해 성능 최적화
  • 실무에서 글로벌 로딩 전략은 모두 지연 로딩
  • 최적화가 필요한 곳은 페치 조인 적용

 

정리

페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.

select m from Member m join fetch m.team

 

여러 테이블을 조인해서 인티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면

페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.

select m, t from Member m join m.team t

 


 

3. 다형성 쿼리

 

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)

예) 부모인 Item과 자식 Book이 있다.

[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’

 


 

4. 엔티티 직접 사용

기본 키 값

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

 

파라미터로 전달하는 경우도 마찬가지로 기본 키 값을 사용한다.

[JPQL]
select m from Member m where m.id = :memberId
select m from Member m where m = :member
[SQL]
select m.* from Member m where m.id=?

 

외래 키 값

연관된 엔티티를 직접 사용하거나 연관된 엔티티의 기본 키 값을 사용해도 SQL에서는 외래 키 값을 사용한다.

[JPQL]
select m from Member m where m.team.id = :teamId
select m from Member m where m.team = :team
[SQL]
select m.* from Member m where m.team_id=?

 


5. Named 쿼리

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

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query="select m from Member m where m.name = :username")
public class Member {
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
        .setParameter("username", "회원1")
        .getResultList();

for (Member member : resultList) {
    System.out.println("username = " + member);
}

애플리케이션 로딩 시점에 Named 쿼리를 SQL로 파싱하기 때문에 사전에 쿼리에 대한 검증이 가능하다.

 

Spring Data JPA의 @Query 어노테이션을 사용할 때 Named 쿼리로 등록된다.

public interface UserRepository extends JpaRepository<User, Long> {

  @Query("select u from User u where u.emailAddress = ?1")
  User findByEmailAddress(String emailAddress);
}

 


 

6. 벌크 연산

JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행되는 경우

쿼리 한 번으로 여러 테이블의 로우를 변경하기 위해 벌크 연산을 사용한다.

 

예) 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

1. 재고가 10개 미만인 상품을 리스트로 조회한다.

2. 상품 엔티티의 가격을 10% 증가한다.

3. 트랜잭션 커밋 시점에 변경감지가 동작한다.

변경 감지를 이용하면 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

 

벌크 연산

int resultCount = em.createQuery("update Member m set m.age = 20")
                .executeUpdate();
  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
  • UPDATE, DELETE 지원
  • INSERT(insert into .. select, 하이버네이트 지원)

 

벌크 연산 주의

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.

영속성 컨텍스트의 엔티티와 벌크 연산으로 변경된 엔티티의 필드 값이 다른 경우 문제가 발생한다.

em.persist(memberA);

int resultCount = em.createQuery("update Member m set m.age = 20")
        .executeUpdate();

// em.clear();

Member findMemberA = em.find(Member.class, memberA.getId());
System.out.println("memberA.age = " + findMemberA.getAge());

JPQL이 실행될 때 영속성 컨텍스트에 저장된 SQL은 함께 flush 되어 나가기 때문에 문제가 없지만

영속성 컨텍스트의 memberA 나이와 벌크 연산으로 수정된 memberA 나이가 달라지는 경우

벌크 연산 이 후 memberA를 조회하면 1차 캐시에서 조회하기 때문에 변경되기 전의 나이가 조회된다.

따라서 영속성 컨텍스트에 엔티티를 영속화 하기 전에 벌크 연산을 먼저 실행하거나

벌크 연산 수행 후 영속성 컨텍스트를 초기화 해주어야 한다.

 

Spring Data JPA에서는 @Modifying의 clearAutomatically 옵션을 통해 영속성 컨텍스트를 초기화 할 수 있다.

clearAutomatically의 기본값은 false이고 true로 설정해 주어야 한다.

package org.springframework.data.jpa.repository;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
public @interface Modifying {
	boolean flushAutomatically() default false;
	boolean clearAutomatically() default false;
}
@Modifying(clearAutomatically = true)
@Query("update User u set u.firstname = ?1 where u.lastname = ?2")
int setFixedFirstnameFor(String firstname, String lastname);

 

 

 

 

 

 

 

 

 

출처

https://www.inflearn.com/course/ORM-JPA-Basic 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한)

 

728x90

'ORM > JPA' 카테고리의 다른 글

[JPA] 조회 성능 최적화  (0) 2023.04.28
[JPA] 8. JPQL(1)  (0) 2022.03.29
[JPA] 7. 값 타입  (0) 2022.03.02
[JPA] 6. 프록시와 연관관계 관리  (0) 2021.12.14
[JPA] 5. 고급 매핑  (0) 2021.12.01