N+1의 근본적인 문제는 ORM과 실제 RDBMS의 차이로 인해 발생하는 성능관련문제다.
다음을 보자.
예를 들어 학생과 선생의 엔티티가 다음과 같은 관계를 가지고 있다고 하자.
@MappedSuperClass
public class person{
@Id
@GeneratedValue
private Long id;
@NotEmpty
private String name;
private int age;
};
@Entity
@Getter
public class Teacher extends person{
@OneToMany(mappedby="student")
List<Student> students=new ArrayList<>();
}
@Entity
@Getter
public class Student extends person{
@ManyToOne(fetch=FetchType.LAZY)
@JoinColum(name="id")
private Teacher teacher
}
Problem 1)다음과 같이 짜여져 있을때 id=1인 학생의 엔티티를 가져올려면 어떻게 해야할까?
Student studeunt=StudentRepository.findOneByid(1);
이때 실행되는 쿼리는 다음과 같다.
SELECT * FROM
Student
student0
INNER JOINTeacher
teacher0
ON student0.id
=teacher0.id
WHERE student0_.id
=1;
하나의 쿼리를 조회할 경우에는 하나의쿼리를 날림으로써 효율적으로 처리함을 알 수 있다.
Problem 2) 모든 학생에 관련한 엔티티를 조회하고 싶을경우는 어떻게 해야하는가?
List<Teacher> teacher = StudentRepository.findAll();
이를 실제 진행되는 쿼리로 나타내면 다음과 같다.
1:
SELECT * FROM `Student` student0;
또한 Student가 가지고 있는 모든 teacher_id (N개에 관해서) 다음과 같이 실행된다.
N:
SELECT * FROM `Teacher` where id=0;
SELECT * FROM `Teacher` where id=1;
SELECT * FROM `Teacher` where id=2;
SELECT * FROM `Teacher` where id=3;
SELECT * FROM `Teacher` where id=4;
따라서 Join을 이용하면 한번에 해결된 문제에 관련하여 JPQL을 이용하였을때 이와같은 N+1의 비효율적은 쿼리가 발생하였다.
사실 FetchType에 관해서 Lazy->Eager로 변경하면 이러한 N+1문제가 해결될거라고 생각하지만 그렇지 않다.
findAll()일경우 Lazy는 다음과 같다.
SELECT * FROM `Teacher` where id=0;
SELECT * FROM `Teacher` where id=1;
SELECT * FROM `Teacher` where id=2;
SELECT * FROM `Teacher` where id=3;
SELECT * FROM `Teacher` where id=4;
다음은 Eager이다.
SELECT * FROM `Teacher` teacher0 LEFT OUTER JOIN `Items` itementity1_
ON teacher0.id=student0.`id` WHERE teacher0.order_id=1;
SELECT * FROM `Teacher` teacher0 LEFT OUTER JOIN `Items` itementity1_
ON teacher0.id=student0.`id` WHERE teacher0.order_id=2;
다음과 같이 쿼리의 형태만 달라졌지 사실 N개는 변함없어 보인다.
Fetch Join을 사용하게 되면 실제 우리가 DBMS에서 데이터를 뽑아낼때 처럼 Join을 사용하게 되면서 하나의 쿼리만 실행한다. 다음과 같이 모든학생에 관한 데이터를 뽑을때 JPQL과 실제쿼리는 다음과 같다.
JPQL쿼리:
select s from Student s join fetch s.Teacher
실제SQL쿼리:
SELECT M.*, T.* FROM Student s INNER JOIN Teacher T ON s.teacher_id = T.id
사실 EnTity Graph의 경우 Fetch Join을 효과적으로 써 코드의 효율성 증가 시키기 위해서 쓰인다. 다음예시를보자
@NamedEntityGraph(
name = "address-city-user-graph",
attributeNodes = {
@NamedAttributeNode("city"),
@NamedAttributeNode("user")
}
)
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String street;
private String flat;
private String postalCode;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "city_id")
private City city;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
// getters and setters
}
@Repository
public interface AddressRepository extends JpaRepository<Address, Long> {
@EntityGraph("address-city-user-graph")
List <Address> findByUserId(Long userId);
@EntityGraph("address-city-user-graph")
List<Address> findByCityId(Long cityId);
}
다음과 같이 @NamedEntityGraph에 city와 user에 관하여 해당 어노테이션을 붙인 메서드가 실행될경우 fetch join이 되게끔 등록해준다.