코딩딩/Spring
JPQL
전낙타
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 m.team from Member m
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인
- 컬렉션을 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야 함
- 경로 탐색은 주로 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와 같음
- 차 이 점
- 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; }
- 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 조인과 fetch type (eager로 설정)은 JPA(Java Persistence API)에서 엔티티 간 관계를 로딩할 때 사용되는 두 가지 다른 개념입니다.
컬렉션 페치 조인
- 일대다 관계, 컬렉션 페치 조인
- JPQL
- select t from Team t join fetch t.members where t.name = ‘TeamA’
- SQL
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 실행
- 재고가 10개 미만인 상품을 리스트로 조회
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작
- 변경된 데이터가 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, 하이버네이트 지원)
벌크 연산 사용 시 주의점
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리
- 벌크 연산을 먼저 실행
- 벌크 연산 수행 후 영속성 컨텍스트 초기화