@Entity
public class Hospital {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long hosId;
// ...
@ManyToOne
@JoinColumn(name = "hos_sigudong")
private Sigudong hosSigudong;
// ...
@OneToMany(mappedBy = "hospital")
private List<HosImg> hosImgs = new ArrayList<>();
// ...
}
//JPQL
select h from Hopital h
N+1
문제가 발생힌다.//SQL
select * from Hopital h
Sigudong
은 FK(시구동의 PK)만 가져올 수 있고,List<HosImg>
는 병원 테이블에서 가져올 수 있는 정보가 전혀 없다.따라서,
병원에 대한 쿼리문1개
,
각 병원의 이미지에 대한 쿼리문N개
,
각 병원의 시구동에 대한 쿼리문N개
발생한다.이와 같은 현상이
N+1 문제
이다.
현재 병원 검색 리스트를 구현 중인데, 한번의 검색결과를 로딩할 때 마다 수십개의 쿼리문이 날라간다...
FetchType
을 설정할 수 있는 속성이 있다.프록시 엔티티 객체
를 필드에 초기화 해놓고실제 엔티티를 상속
받아서 만들어지는 프록시 엔티티
지연 로딩
em.getReference()
target
필득 null인 상태엔티티 or 컬렉션
을 한번의 쿼리문으로 조회할 수 있도록 한다.join fetch
문을 사용@ManyToOne
private Sigudong hosSigudong;
select h
from Hospital h
join fetch h.sigudong
select h.*, s.*
from Hospital h
inner join Sigudong s
on h.sigudong_id = s.sigudong_id
@OneToMany(mappedBy = "hospital")
private List<HosImg> hosImgs = new ArrayList<>();
select h
from Hospital h
join fetch h.hosImgs
select i.*, h.*
from Hospital h
inner join Hos_Img i
on h.hospital_id = i.hospital_id
1
쪽이고 조인하는 테이블이 N
쪽이기 때문에 원래 조회할 레코드보다 많은 레코드를 가져오게된다.List<Hospital> result = em.createQuery("...", Hospital.class)
.getResultList();
// List에 담기는 엔티티 정보
// {id = 1, hosImgs = {{id = 1, ...}, {id = 2, ...}, {id = 3, ...}}
// {id = 1, hosImgs = {{id = 1, ...}, {id = 2, ...}, {id = 3, ...}}
// {id = 1, hosImgs = {{id = 1, ...}, {id = 2, ...}, {id = 3, ...}}
// {id = 2, hosImgs = {{id = 4, ...}, {id = 5, ...}}
// {id = 2, hosImgs = {{id = 4, ...}, {id = 5, ...}}
// {id = 3, hosImgs = {{id = 6, ...}}
for (Hospital hospital : result) {
System.out.println(hospital)
}
// List의 각 요소의 객체 주소지
// Hospital@0x100 (1번 병원)
// Hospital@0x100 (1번 병원)
// Hospital@0x100 (1번 병원)
// Hospital@0x200 (2번 병원)
// Hospital@0x200 (2번 병원)
// Hospital@0x300 (3번 병원)
HosImg
와 관련된 정보는 각각의 사진이 해당이되므로 첫번째 1번병원, 두번쨰 1번병원, 세번째 1번병원
은 다른 레코드이다.hopital_id (PK) | hospital_name |
1 | 1번 병원 |
2 | 2번 병원 |
3 | 3번 병원 |
병원 사진
hosImg_id (PK) | hosImg_path | hospital_id (FK) |
1 | 1번 사진 | 1 |
2 | 2번 사진 | 1 |
3 | 3번 사진 | 1 |
4 | 4번 사진 | 2 |
5 | 5번 사진 | 2 |
6 | 6번 사진 | 3 |
SQL 조회 결과
hopital_id | hospital_name | hosImg_id | hosImg_path |
1 | 1번 병원 | 1 | 1번 사진 |
1 | 1번 병원 | 2 | 2번 사진 |
1 | 1번 병원 | 3 | 3번 사진 |
2 | 2번 병원 | 4 | 4번 사진 |
2 | 2번 병원 | 5 | 5번 사진 |
3 | 3번 병원 | 6 | 6번 사진 |
순수 SQL의 distinct의 명령어로는 중복을 제거하지 못한다.
애플리케이션 메모리 상에서 중복 제거를 추가적으로 해준다.
List<Hospital>
에 엔티티 객체가 매핑될 때는 1번 병원은 다 같은 객체이므로 중복을 제거 해준다.
List<Hospital> result = em.createQuery("select distinct h from Hospital h fetch join h.hosImgs", Hospital.class)
.getResultList();
// List에 담기는 엔티티 정보
// {id = 1, hosImgs = {{id = 1, ...}, {id = 2, ...}, {id = 3, ...}}
// {id = 2, hosImgs = {{id = 4, ...}, {id = 5, ...}}
// {id = 3, hosImgs = {{id = 6, ...}}
페치 조인 대상에 별칭을 줘서, 그 별칭을 통하여 조건을 줘서 컬렉션에 일부의 데이터만을 담는다는 것은 JPA의 엔티티의 방향성과 맞지 않는다. 엔티티 필드의 컬렉션에는 연관관계에 해당되는 모든 정보가 담겨 있어야 의미가 있다.
둘 이상의 컬렉션 페치 조인을 사용하면 데이터가 엄청나게 늘어날 위험이 있어서 사용하지 않는 편이 낫다.
@BatchSize(size = 50)
@OneToMany(mappedBy = "hospital",orphanRemoval = true)
private List<HosImg> hosImgs = new ArrayList<>();
em.createQuery("select h " +
"from Hospital h "+
"where h.hosName LIKE :keyword", Hospital.class)
.setParameter("keyword", "%"+ 검색어 +"%")
.setFirstResult(시작)
.setMaxResults(총 개수)
.getResultList();
@Entity
public class Sigudong {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long sigudongId;//1124900000
private String sigudongName; //용산구
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id")
private Sigudong parent; //서울특별시 엔티티
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<Sigudong> child = new ArrayList<>();
}
시
의 하위 주소로 구
들이 속해있는 경우return em.createQuery("select distinct si " +
"from Sigudong si join fetch si.child " +
"where si.parent is null ", Sigudong.class)
.getResultList();
@Entity
public class Hospital {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long hosId;
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "hos_sigudong")
private Sigudong hosSigudong;
// ...
@BatchSize(size = 50)
@OneToMany(mappedBy = "hospital",orphanRemoval = true)
private List<HosImg> hosImgs = new ArrayList<>();
// ...
}
실제 발생 쿼리
N+1번의 쿼리문이 날라가던 문제를 2개의 쿼리로 해결할 수 있다.