EntityGraph 란?
연관관계가 있는 엔티티를 조회할 경우, 지연로딩으로 설정되어 있다면 연관관계에서 종속된 엔티티는
쿼리 실행 시, select 쿼리 대신 proxy 객체를 만들어 엔티티가 적용시킨다.
그 후, 해당 proxy 객체가 호출될 때마다 select 쿼리가 전송된다.
@EntityGraph 는 연관관계가 지연로딩으로 되어있을 때 fetch 조인을 사용하여 여러 번의 쿼리를 한 번에 해결할 수
있는 점에서 fetch-join 을 어노테이션을 통해 사용할 수 있도록 한 기능이다.
(fetch-join은 일반 join 과 달리 연관 엔티티도 함께 영속 상태가 된다는 점이 있다.)
예를 들어 다음과 같은 엔티티들과 연관관계가 있다고 가정해 보자.
@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Owner {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private String name;
@Builder.Default
@OneToMany(mappedBy = "owner", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<Company> companies = new ArrayList<>();
}
@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Company {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Owner owner;
}
(*ManyToOne 의 기본 fetch 전략은 EAGER(즉시 로딩) 방식이다.)
이어서 다음과 같은 테스트 작성한 후, 발생한 SQL 들을 로그로 보면
@Test
void saveCompany() {
// given
companyDao.save(company("Lobotomy Corporation"));
companyDao.save(company("Procyon Industries"));
// when
List<Company> all = new ArrayList<>();
companyDao.findAll().forEach(all::add);
// verify
assertThat(all.size()).isEqualTo(2);
}
Hibernate: insert into company (name, owner_id) values (?, ?)
Hibernate: insert into company (name, owner_id) values (?, ?)
Hibernate: select c1_0.id,c1_0.name,c1_0.owner_id from company c1_0
(설정마다 다르겠으나, 필자의 경우에는 위와 같이 나왔다.)
로그를 보면 알듯이 owner 객체를 select 하지 않았다. (물론 company 칼럼의 owner_id 는 제대로 조회하였다.)
select 쿼리가 한 번만 발생하며, Company 객체 내에서 Owner 객체에 접근하기 전까지 Owner 에 대한 쿼리를
수행하지 않기 때문이다.
EnttiyGraph 를 사용한 fetch-join
Repository(DAO) 에 필요한 부분에 @EntityGraph 를 붙여 사용할 수 있다. 다음과 같이 사용할 수 있다.
@Override
@EntityGraph(attributePaths = "owner")
Iterable<Company> findAll();
attributePaths 인자에 fetch-join 을 할 대상(필드명)을 작성하여 해당 엔티티들을 fetch-join 할 수 있다.
변경된 DAO를 사용하여, 그대로 테스트 코드를 진행한 결과는 다음과 같다.
Hibernate: select c1_0.id,c1_0.name,o1_0.id,o1_0.name from company c1_0 left join owner o1_0 on o1_0.id=c1_0.owner_id
insert 쿼리는 동일하기에 임의로 작성하지 않았다.
위 테스트 로그와 달리 left join 이 발생하여 owner 역시 select 하였음을 볼 수 있다. 이는 즉시로딩 시 발생하는 쿼리와 유사하다. (혹은 동일할 수도 있다.)
(물론 현재 테스트에서는 owner 를 따로 지정하지 않았기에 실제로 테스트할 경우, left join 이 발생하지 않는다.)
NamedEntityGraph
잘 사용하지 않는 방법 중 하나라고 하는 데, 이유를 Entity 클래스에 직접 작성해주어야 하기 때문이라고 한다.
다음과 같이 사용하면 된다고 한다. (필자는 한 번도 사용해 본 적이 없으며, 공식문서상에서만 보았다.)
@Entity
@NamedEntityGraph(name = "company.findAll", attributeNodes = @NamedAttributeNode("owner"))
@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Company {
// .. 생략
}
@Query("select c from Company c")
@EntityGraph("company.findAll")
List<Company> findAllCompanies();
위 테스트 결과와 동일한 쿼리가 발생하였다.
추가
@EntityGraph 의 type 은 두 가지가 있다. 그중 FETCH 가 default 이다.
- FETCH: @EntityGraph 에 명시한 attribute 는 EAGER 로 패치한다. 나머지는 LAZY 로 패치한다.
- LOAD: @EntityGraph 에 명시한 attribute 는 EAGER 로 패치한다. 나머지는 Entity 에 명시한 fetch type을 따른다.
이상입니다.
'Spring > JPA' 카테고리의 다른 글
[Spring/JPA] JPA Auditing (0) | 2023.02.17 |
---|---|
[Spring/JPA] 엔티티의 생명주기 (0) | 2023.02.16 |
[Spring/JPA] 벌크 연산(Bulk Operation)과 EntityGraph (0) | 2023.02.13 |
[Spring/JPA] JPA 연관관계 (0) | 2023.02.13 |
[Spring/JPA] JPA(Hibernate 구현체) DDL 전략 (0) | 2022.06.17 |