Club.java
package hellojpa;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
@Data
@Entity
public class Club {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "club")
private List<Student> students = new ArrayList<>();
}
Student.java
package hellojpa;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Data
public class Student {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private int age;
@Setter(AccessLevel.PROTECTED)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CLUB_ID")
Club club;
void changeClub(Club club) {
this.setClub(club);
club.getStudents().add(this);
}
}
JpaMain.java - club 및 student data 저장 로직
Club c1 = new Club();
c1.setName("c1");
em.persist(c1);
Club c2 = new Club();
c2.setName("c2");
em.persist(c2);
Student student = new Student();
student.setName("code-mania");
student.setAge(21);
student.changeClub(c1);
em.persist(student);
Student student2 = new Student();
student2.setName("code-mania2");
student2.setAge(21);
student2.changeClub(c1);
em.persist(student2);
Student student3 = new Student();
student3.setName("code-mania3");
student3.setAge(21);
student3.changeClub(c2);
em.persist(student3);
em.flush();
em.clear();
N+1
JpaMain.java를 실행하면 다음과 같이 데이터가 생성된다.
JpaMain.java - students 조회 로직
//일반조인
String query = "select s from Student s join s.club c";
List<Student> students = em.createQuery(query, Student.class).getResultList();
for (Student s : students) {
System.out.println("name = " + s.getName());
System.out.println("club = " + s.getClub().getName());
System.out.println("================================");
}
위와 같이 작성한 로직에서 쿼리는 몇 번 나가게 될까?
JpaMain.java 실행 후 로그
Hibernate: select s.* from Student s inner join Club c on s.CLUB_ID=c.id
name = code-mania
Hibernate: select c.id, c.name from Club c where c.id=?
club = c1
================================
name = code-mania2
club = c1
================================
name = code-mania3
Hibernate: select c.id, c.name from Club c where c.id=?
club = c2
================================
로그를 살펴보면 student를 조회하는 쿼리가 제일 먼저 나갔다.
그 후 club을 join했음에도 지연로딩 설정으로 인해
영속성 컨텍스트에 없는 club을 조회할 때마다 select문이 나가게 된다.
이 때 최대로 나갈 수 있는 쿼리 개수는 N(조회된 총 데이터 수) + 1이다.
N + 1은 성능을 굉장히 저하시키는 문제이다.
이를 fetch join을 통해 해결할 수 있다.
Fetch join
JpaMain.java - 조회 로직
//페치조인
String query = "select s from Student s join fetch s.club c";
List<Student> students = em.createQuery(query, Student.class).getResultList();
for (Student s : students) {
System.out.println("name = " + s.getName());
System.out.println("club = " + s.getClub().getName());
System.out.println("================================");
}
JpaMain.java 실행 후 로그
Hibernate: select s.*, c.* from Student s
inner join Club c on student0_.CLUB_ID=club1_.id
name = code-mania
club = c1
================================
name = code-mania2
club = c1
================================
name = code-mania3
club = c2
================================
fetch 조인을 사용하면 지연로딩으로 설정되어있는 연관필드를 한 번의 쿼리로 들고 올 수 있다.
결과적으로 fetch 조인을 통해 N + 1 문제가 해결되었다.
컬렉션 페치조인
JpaMain.java - 조회 로직
//컬렉션페치조인
String query = "select c from Club c join fetch c.students s";
List<Club> clubs = em.createQuery(query, Club.class).getResultList();
for (Club c : clubs) {
System.out.println("name = " + c.getName());
System.out.println("student size = " + c.getStudents().size());
System.out.println("================================");
}
H2 DB에 데이터가 위와 같고,
JPQL을 보면 club에서 collection 연관필드인 students를 경로탐색을 통해 조회하고 있다.
JpaMain.java 실행 후 로그
Hibernate: select c.*,s.* from Club c inner join Student s on c.id=s.CLUB_ID
name = c1
student size = 2
================================
name = c1
student size = 2
================================
name = c2
student size = 1
================================
club 테이블에는 2개의 데이터만이 있지만,
조회결과가 담긴 clubs 변수의 size는 3인 것을 알 수 있다.
왜 이런 것인지 확인해보기 위해 h2에서 직접 inner join을 해보자!
데이터가 3개다. 왜냐하면 join한 student 테이블 중 club_id가 설정되어있는 행이 3개이기때문이다.
(club_id가 null이거나 club에 없는 값이면 inner jon이기때문에 조회되지 않는다)
이를 JPA가 들고 오면 우리가 로그에서 봤듯이 3개의 데이터가 조회된다.
그리고 club name이 c1인 Club 엔티티가 중복저장되어 있다는 것을 알 수 있다.
이러한 중복저장을 제거하기 위해서는 DISTINCT 키워드를 사용하면 된다.
DISTINCT
JPQL에서 DISTINCT는 2가지 기능을 제공한다.
- SQL에 DISTINCT가 적용된다.(당연하다고 볼 수 있다)
- 애플리케이션에서 중복되는 엔티티를 제거한다.
지금 c1이 중복되고 있는데, DISTINCT를 사용하면 c1이 중복되지 않게 된다.
JpaMain.java - 조회 로직
//컬렉션페치조인
String query = "select distinct c from Club c join fetch c.students s";
List<Club> clubs = em.createQuery(query, Club.class).getResultList();
for (Club c : clubs) {
System.out.println("name = " + c.getName());
System.out.println("student size = " + c.getStudents().size());
System.out.println("================================");
}
JpaMain.java 실행 후 로그
Hibernate: select distinct c.*, s.* from Club c inner join Student s on c.id=s.CLUB_ID
name = c1
student size = 2
================================
name = c2
student size = 1
================================
DISTINCT 키워드를 사용하면 중복되던 c1이 중복되지 않는 것을 확인할 수 있다.
'JPA > JPQL' 카테고리의 다른 글
JPQL 엔티티 조회 (0) | 2021.10.15 |
---|---|
페치조인 특징과 한계 (0) | 2021.10.12 |
경로표현식 (0) | 2021.10.02 |
JPQL 함수 (0) | 2021.09.25 |
JPQL에서의 Enum과 조건식 (0) | 2021.09.17 |