티스토리 뷰
JOIN 의 역할
JPQL 에서 JOIN은 SQL 에서의 JOIN 과 차이가 없다. JOIN 은 항상 FROM 절 뒤에 붙는다. FROM 이 자바의 향상된 반복문에서 콜론 (:) 같은 역할이라고 생각하면 된다. 이 때 JOIN 은 반복하는 구간을 특정하는 역할을 한다. 예를 들어, 다음 두 개는 같은 역할을 하는 쿼리문들이다.
SELECT c1, c2 FROM Country c1 INNER JOIN c1.neighbors c2
SELECT c1, c2 FROM Country c1, Country c2
WHERE c2 MEMBER OF c1.neighbors
즉, INNER JOIN 은 두 항목을 조회하되, 하나는 연관관계에서 주인인 테이블의 항목이고, 다른 하나는 주인에 의해 매핑된 테이블의 항목으로 제한시켜 조회하는 것이다. 매핑된 테이블 항목이 존재하지 않으면 사용할 수 없다.
위의 두 쿼리문들 중에서는 JOIN 을 사용하는 것을 더 권장한다. 짧고 간결해지는 것 외에, DBMS 최적화에 의지하지 않아도 이미 바깥 반복문 (outer loop) 과 안쪽 반복문 (inner loop) 을 사용하는 올바르고 효율적인 방법이기 때문이다.
JOIN, LEFT / RIGHT OUTER JOIN, JOIN FETCH 의 차이
자, 이제 범위를 좀 더 넓혀서 JPQL 에서 사용되는 다양한 JOIN의 종류들에 대해 알아보자. 이름에 따라 역할이 상이하다.
(INNER) JOIN | LEFT (OUTER) JOIN | RIGHT (OUTER) JOIN | JOIN FETCH |
두 테이블 중 하나의 테이블의 내용을 다른 테이블 항목들과의 연관관계를 기준으로 조회할 때 사용된다. | 왼쪽에 명시된 테이블의 내용을 오른쪽의 테이블에 연관관계가 있는 내용과 함께 조회할 때 사용된다. |
오른쪽에 명시된 테이블의 내용을 왼쪽의 테이블에 연관관계가 있는 내용과 함께 조회할 때 사용된다. | 하나의 테이블을 조회할 때 연관관계를 가진 테이블을 함께 데이터베이스에서 가져오고 싶을 때 사용된다. |
매칭되는 Inner variant 가 없는 Outer variant 를 무시 예) Country 과 Capital 을 INNER JOIN 하면 Captial 이 없는 Country는 무시 |
INNER JOIN 과 다르게, 매칭되는 Inner variant 가 없다면 NULL 값을 할당 예) Country 중 Capital 이 없다면 무시하지 않고 Capital 에 NULL 을 할당 |
다른 JOIN들과는 다르게 변수가 아니라 엔티티 클래스명만 명시 |
JOIN FETCH 의 예시
Country 라는 엔티티 클래스에 Capital 이라는 변수가 있고 이 변수는 @OneToOne 으로 Capital 엔티티 클래스와 연관관계를 나타내고 있다고 가정한다. 이 상황에서 아래 쿼리문을 실행한다:
SELECT c FROM Country c
이 때 Country 클래스의 변수들을 데이터베이스에서 모두 조회하지만, Capital 객체 변수는 주소값만 나올 것이다. 이 쿼리문만으로는 데이터베이스에서 Capital 테이블은 가져오지 못하기 때문이다. 그래서 Country 들의 Capital 을 구하려면 아래와 같이 해야 한다:
List<Country> results = query.getResultList();
for (Country c : results) {
System.out.println(c.getName() + " => " + c.getCapital().getName());
}
이렇게 되면 Country 내의 Capital 객체를 반복문 내의 리스트를 돌며 한 번에 하나씩 가져온다. 한 번에 모두 가져온 다음에 getName 으로 이름을 조회하는 방법이 효율적인데 말이다. 이런 상황에서 JOIN FETCH 를 사용할 수 있다.
아래 쿼리문은 위의 작업을 한 줄로 해결해준다:
SELECT c FROM Country c JOIN FETCH c.capital
이 쿼리문 한 줄로 Country 내의 captial 을 모두 한 번에 가져온다. 하나씩 가져오지도, Country 를 가져온 후에 Capital 을 한 번 더 가져오지도 않기 때문에 훨씬 효율적이다.
N+1 문제
N+1 문제는 JPQL 에서 1개의 쿼리를 생성해야 되는 상황에서 N 개의 쿼리를 더 생성하는 문제를 일컫는다.
원인은 Fetchtype.EAGER 상태에서 JOIN FETCH 을 사용하지 않는 것이다.
가장 주요 원인은 연관관계를 설정할 때 붙인 어노테이션에서 fetch = Fetchtype.EAGER 을 명시해준 상태에서 JOIN FETCH 를 사용하지 않는 것이다. Fetchtype.EAGER 은 @ManyToOne 이나 @OneToOne 연관관계를 가진 엔티티에 디폴트로 설정되어 있는 Fetch 방식인데, 모든 것을 다 가져오고 본다. 다음과 같은 엔티티 클래스들이 있다고 가정하자:
@Entity
@Table(name = "country")
public class Country {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
private String name;
//Getter와 Setter 들은 생략했다.
}
@Entity
@Table(name = "city")
public class City {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Country country;
private Long phoneCode;
//Getter와 Setter 들은 생략했다.
}
여기서 City 엔티티를 조회하고자 하면, 여러 개의 City 객체를 하나 조회할 때마다 City 와 연관관계에 있는 country 항목들이 모두 조회된다. 즉 City 객체가 10개이면 10 x (country 테이블 내의 항목 수) 만큼 조회된다. 실제로 필요한 항목은 그 때마다의 City 객체와 연관 있는 country 테이블 항목 하나 뿐인데, 그 하나를 찾기 전에 일단 모든 연관관계가 있는 항목들을 모두 불러오기 때문에 매우 비효율적이다. 실제로 한 번 조회해보자. API는 다음과 같다:
@PostMapping("/test")
public ResponseEntity<List<City>> getAllCities() {
return ResponseEntity.ok().body(cityService.findAllCities());
}
다음과 같은 EntityManager 를 JpaRepository 를 상속받도록 해서 생성한다:
import com.springboot.relationship.domain.entity.City;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CityEntityManager extends JpaRepository<City, Long> {
}
서비스에서 컨트롤러에서 사용할 메서드를 정의해 두었다:
import com.springboot.relationship.domain.entity.City;
import com.springboot.relationship.repository.CityEntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CityService {
private final CityEntityManager cityEntityManager;
public List<City> findAllCities() {
return cityEntityManager.findAll();
}
}
다음은 Fetchtype.EAGER 상태에서 로그에 기록된 Hibernate 가 생성한 쿼리이다:
Hibernate: select city0_.id as id1_2_, city0_.country_id as country_3_2_, city0_.phone_code as phone_co2_2_ from city city0_
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
이번에는 City 클래스의 Country 변수 위 어노테이션을 @ManyToOne(fetch = Fetchtype.LAZY) 로 바꾼 뒤 실행해본다:
InvalidDefinitionException 이 발생해버렸다. 이하 생략한다. 해결 방법은 이후에 포스팅하도록 하겠다.
어쨌든 이것이 Fetchtype.LAZY 를 쓰는 이유이다. 연관관계가 있는 항목들을 미리 모두 불러오지 않고, 미뤄두었다가 나중에 쿼리문을 통해 그 항목들을 실제로 조회할 때에만 불러오는 것이다. 어쨌든, JPQL 에서는 이 문제를 JOIN FETCH 를 통해 해결한다. 필요한 항목들을 한 번에 불러놓고 찾을 수 있게 되기 때문이다.
출처
https://www.objectdb.com/java/jpa/query/jpql/from
'Database' 카테고리의 다른 글
[Database Test] Soft Delete 를 H2 활용해서 테스트하기 (0) | 2023.01.10 |
---|---|
[MySQL] date vs datetime vs timestamp (2) | 2022.12.03 |
[Hibernate] Session 이란? (0) | 2022.11.26 |
[Spring Data JPA] LazyInitializationException 2부: Hibernate 의 List 구현 방식 & 지연 로딩 (0) | 2022.11.26 |
[Spring Data JPA] LazyInitializationException (0) | 2022.11.26 |
- Total
- Today
- Yesterday
- gitlab
- google cloud
- DeSerialization
- Java Data Types
- 지연 로딩
- JPA
- DTO
- LazyInitializationException
- 깃랩
- 실시간데이터
- JPQL
- 가상 서버
- 인증/인가
- Spring Boot
- json web token
- N+1
- 기지국 설치
- 프로그래머스
- Firebase
- JOIN FETCH
- FCM
- 알고리즘
- docker
- @RequestBody
- 역직렬화
- ci/cd
- 도커
- 코테
- Jackson
- spring
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |