전낙타 2023. 11. 18. 17:33

경로 표현식

  • 점을 찍어 객체 그래프를 탐색하는 것 (m.member t)

경로 표현식 용어 정리

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

경로 표현식 특징

  • 상태 필드 (state field) : 경로 탐색의 끝, 탐색 X
  • "select m.username From Member m";
  • 단일 값 연관경로 : 묵시적 내부 조인 (inner join) 발생, 탐색 O
  • "select m.team.id From Member m";
  • 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 X
  • "select t.members From Team t";

명시적 조인, 묵시적 조인

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

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

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

JPQL - 페치 조인 (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
for (Member member : resultList) {
    System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
//  기본적으로 연관관계가 걸려있는 컬럼은 모두 Fetch type이 LAZY로 걸려있다.
//  그렇기 때문에 member를 조회해 오면 member와 연관관계인 team은 프록시로 호출해온다.
//  이렇게 되면 처음 member를 호출할때 쿼리 한방, member가 가지고 있는 team을 조회하기 위해 쿼리 한방
//  마지막으로 다른 팀 소속인 member의 team을 조회할때 쿼리 한방 총 3방이 나가게 된다
//  이 문제를 N+1 문제라고 한다.
//  이를 해결하기 위해 fetch 조인을 사용
}
String query = "select m From Member m join fetch m.team";

List<Member> resultList = em.createQuery(query, Member.class)
        .getResultList();

for (Member member : resultList) {
    System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}
// select 쿼리가 한번만 나가게 된다.
// fetch 조인은 member를 가져올때 team을 프록시가 아닌 엔티티를 가져오게 되서 N+1 문제가 발생하지 않는다.
// 이러면 fetch type을 EAGER로 주는것과 무슨 차이가 있는지 찾아보자
// fetch join은 내가 원하는 쿼리에 동적으로 줄 수 있다는 장점이 있다. 작동 메커니즘은 EAGER와 같음
  • 차 이 점
    1. Fetch 조인:
      • fetch 조인은 JPQL 쿼리나 Criteria API를 사용하여 데이터베이스에서 엔티티와 관련된 다른 엔티티를 함께 로딩하는 방법입니다.
      • 기본적으로 JPA는 연관된 엔티티를 지연 로딩(**fetch type**이 **LAZY**로 설정)합니다. 이 경우, 실제로 해당 엔티티를 사용할 때에야 데이터베이스에서 로딩이 발생합니다.
      • fetch 조인을 사용하면 쿼리를 실행할 때 관련된 엔티티도 함께 로딩하므로 추가적인 쿼리가 발생하지 않습니다. 이는 성능상의 이점을 가질 수 있습니다.
      • 하지만, 주의할 점은 fetch 조인을 남용하면 성능 문제를 야기할 수 있으며, 불필요한 데이터를 로딩할 우려가 있습니다.
    javaCopy code
    @Entity
    public class Team {
        @OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
        private List<Member> members;
    }
    
    
    1. Fetch Type을 Eager로 설정:
      • **fetch type**은 엔티티 클래스에서 어떻게 연관된 엔티티를 로딩할지를 설정하는데 사용됩니다.
      • **eager**로 설정하면, 부모 엔티티를 로딩할 때 연관된 자식 엔티티도 함께 로딩됩니다. 즉, 지연 로딩 대신 즉시 로딩이 이루어집니다.
      • 이는 일반적으로 부모 엔티티와 연관된 자식 엔티티를 항상 사용한다고 가정할 때 유용합니다. 그러나 주의할 점은 부모 엔티티를 로딩할 때 항상 관련된 모든 자식 엔티티도 로딩되므로, 불필요한 데이터를 가져올 수 있습니다.
    javaCopy code
    @Entity
    public class Team {
        @OneToMany(mappedBy = "team", fetch = FetchType.EAGER)
        private List<Member> members;
    }
    
    차이점 요약:
    • fetch 조인은 쿼리 수행 시에 함께 로딩되도록 지시하는 방법이며, JPQL이나 Criteria API에서 사용됩니다.
    • **fetch type**이 **eager**로 설정되면 항상 로딩되는 것을 의미하며, 엔티티 클래스에 설정됩니다.
    • 두 가지 방법 모두 사용 시에는 주의해서 사용해야 하며, 성능 이슈와 불필요한 데이터 로딩을 방지하기 위해 조절하는 것이 중요합니다.
  • fetch 조인과 fetch type (eager로 설정)은 JPA(Java Persistence API)에서 엔티티 간 관계를 로딩할 때 사용되는 두 가지 다른 개념입니다.

컬렉션 페치 조인

  • 일대다 관계, 컬렉션 페치 조인
  • JPQL
    • select t from Team t join fetch t.members where t.name = ‘TeamA’
  • SQL
    • select t., m. from team t inner join member m on t.id = m.team_id where t.name = ‘teamA’
String query = "select t From Team t join fetch t.members";

List<Team> result = em.createQuery(query, Team.class)
        .getResultList();

for (Team team : result) {
    System.out.println("team = " + team.getName() + " | members = " + team.getMembers());
}
// 하이버네이트 버전에 따라 중복된 데이터가 자동으로 삭제될 수 있다.
  • 강의와 버전이 달라 고생했던 부분Starting with Hibernate ORM 6 it is no longer necessary to use distinct in JPQL and HQL to filter out the same parent entity references when join fetching a child collection. The returning duplicates of entities are now always filtered by Hibernate.
  • 하이버네이트6 부터는 distinct 명령어를 사용하지 않아도 엔티티의 중복을 제거하도록 변경되었다. 난 그것도 모르고 애꿎은 join fetch 만 만지작 거리고 있었다.

페치 조인의 특징과 한계

  • 페치 조인 대상에는 별칭을 줄 수 없다.
    • 하이버네이트는 가능, 가급적 사용 X
  • 둘 이상의 컬렉션을 페치 조인할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults) 를 사용할 수 없다.
    • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
    • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험)

JPQL 엔티티 직접 사용

  • JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
  • [JPQL]
    • select count(m.id) from Member m // 엔티티의 아이디를 사용
    • select count(m) from Member m // 엔티티를 직접 사용
  • 둘다 똑같이 id로 조회하는 SQL이 나가게 된다.
String query = "select m from Member m where m.id = :memberId";
Member singleResult = em.createQuery(query, Member.class)
        .setParameter("memberId", member1.getId())
        .getSingleResult();

System.out.println("singleResult = " + singleResult);

Named 쿼리 - 정적쿼리

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증

아래와 같은 방법으로 엔티티에 정의해주면 된다.

package org.example.jpql;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Getter
@Setter
@ToString
@NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"
)
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String username;
    private int age;

    @Enumerated(EnumType.STRING)
    private MemberType type;

    @ManyToOne
    @JoinColumn(name = "team_id")
    @ToString.Exclude
    private Team team;

    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

사용은 이렇게 createNamedQuery를 정의해주고 사용하면 된다.

Member singleResult = em.createNamedQuery("Member.findByUsername", Member.class)
      .setParameter("username", member1.getUsername())
      .getSingleResult();

System.out.println("singleResult = " + singleResult);
  • XML이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다

벌크 연산

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

이렇게만 두고봐도 정말 많은 리소스를 잡아먹을것이라는것을 알 수 있다.

이를 해결하기 위한 방법이 벌크연산

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

System.out.println("executeUpdate = " + executeUpdate);
  • 쿼리 한번으로 여러 테이블 로우 변경 (엔티티)
  • executeUpdate() 의 경과는 영향받은 엔티티 수 반환
  • Update, Delete 지원
  • Insert (insert info …. select, 하이버네이트 지원)

벌크 연산 사용 시 주의점

  • 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
    • 벌크 연산을 먼저 실행
    • 벌크 연산 수행 후 영속성 컨텍스트 초기화