오직 Spring-Data-JPA 만 사용하며 N+1을 최소화하고 연관된 데이터들을 한번에 조회하고 싶을때가 있다
여러가지 해결방법 중 Entity Graph를 사용하는 방법을 다뤄본다.
Entity Graph를 사용하여 FetchType이 Lazy 인 연관관계 엔티티들을 한번에 (Left Join 으로) 조회할 수 있다.
(fetch join을 사용하고 싶으면 JPQL을 사용하여 fetch join 을 직접 사용하자.)
@EntityGraph 사용
간단한 연관관계는 다음과 같이 Repository 에서 @EntityGraph 를 사용하여 사용이 가능하다.
// 부모 Entity
@Entity
...
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "P_ID", nullable = false)
private long id;
@Size(max = 100)
@NotNull
@Column(name = "P_NM", nullable = false, length = 100)
private String parentName;
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Child> childEntity = new ArrayList<>();
}
// 자식 Entity
@Entity
...
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "C_ID", nullable = false)
private long cId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "P_ID")
private Parent parent;
}
// 부모 Repository
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {
@EntityGraph(attributePaths = {"child"}, type = EntityGraph.EntityGraphType.LOAD)
Optional<Parent> findById(Long parentId);
}
attributePaths : 연관관계의 변수 명을 배열로 지정
type - FETCH : entity graph attribute는 EAGER 로 패치하고, 나머지는 LAZY로 패치
type - LOAD : entity graph attribute는 EAGER로 패치하고, 나머지는 entity에 명시한 FetchType 이나 default FetchType으로
@NamedEntityGraph 정의
부모와 자식의 관계(1depth) 뿐만 아니라 부모-자식-자식 형태의 (2depth) 의 연관관계도 심심치 않게 볼 수 있다.
이럴때는 엔티티 그래프를 직접 정의하여 조회가 가능하다.
@NamedEntityGraph(
name = "ParentGraph",
attributeNodes = {
@NamedAttributeNode(value = "childEntity", subgraph = "child-toy"),
@NamedAttributeNode(value = "homeEntity")
},
subgraphs = {@NamedSubgraph(
name = "child-toy",
attributeNodes = {
@NamedAttributeNode("toyEntity")
}
)}
)
위 엔티티는 부모와 집이 일대일 단방향 관계로
그리고 부모와 자식이 일대다 양방향 관계, 자식과 장난감이 일대일 단방향 관계로 있는 엔티티이다.
@NamedAttributeNode 의 subgraph 속성에 @NameSubgraph 에서 사용할 이름을 지정하고
@namedSubgraph의 attributeNodes 속성에서 자식의 연관관계 엔티티 명을 작성한다
// 부모 Entity
@Entity
...
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "P_ID", nullable = false)
private long id;
// 단방향
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "HOME")
private Home homeEntity;
// 양방향
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Child> childEntity = new ArrayList<>();
}
// 자식 Entity
@Entity
...
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "C_ID", nullable = false)
private long cId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "P_ID")
private Parent parent;
//단방향
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "toy")
private TOY toyEntity;
}
// 부모 Repository
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {
@EntityGraph(value = {"ParentGraph"}, type = EntityGraph.EntityGraphType.LOAD)
Optional<Parent> findById(Long parentId);
}
@NamedEntityGraph 로 정의한 Entity Graph의 명을 value 속성에 정의하여 사용한다
엔티티에 일대다 연관관계와 다대일 연관관계가 같이 있을경우
엔티티에 연관관계 (~ to many) 두 개 이상 있는경우에 Entity Graph 를 사용하면 (fetch join도 동일)
MultipleBagFetchException 에서 Multiple Bag 는 collection join이 두 개 이상이라는 의미
// 부모 Entity
@Entity
...
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "P_ID", nullable = false)
private long id;
// 양방향
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Child> childEntity = new ArrayList<>();
// 양방향
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Child> dogEntity = new ArrayList<>();
}
// 부모 Repository
@Repository
public interface ParentRepository extends JpaRepository<Parent, Long> {
@EntityGraph(attributePaths = {"childEntity", "dogEntity"})
List<Parent> findAll();
}
다음과 같이 Parent 를 전체조회 하면 MultipleBagFetchException 이 발생한다
(NamedEntityGraph에 정의되어 있는 ~toMany 관계가 많아도 멀티백 패치 예외가 발생한다)
1. List형을 Set으로 변환
해결방법은 다음과 같이 자료형을 Set 형태로 변환해주는 방법이다.
위와 같은 방법은 데이터의 순서를 보장할수 없기 때문에 다시 정렬이 필요하다
// 부모 Entity
@Entity
...
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "P_ID", nullable = false)
private long id;
// 양방향
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Child> childEntity = new HashSet<>();
// 양방향
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Set<Child> dogEntity = new HashSet<>();
}
2. batch Size 적용
@BatchSize를 List 혹은 Set 위에 정의하면 인메모리에 가져오는 것이 아닌 호출하는 당시에 한번에 모든 데이터를 가져오는 동작구조를 가진다
// 부모 Entity
@Entity
...
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "P_ID", nullable = false)
private long id;
// 양방향
@BatchSize(size = 100)
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Child> childEntity = new ArrayList<>();
// 양방향
@BatchSize(size = 100)
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<Child> dogEntity = new ArrayList<>();
}
주의해야할 점
batch size에 fetch join을 걸면 안된다. (@Query 어노테이션 사용시)
fetch join이 우선시되어 적용되기 때문에 batch size가 무시되고 fetch join을 인메모리에서 먼저 진행하여 List가 MultipleBagFetchException가 발생
'Spring Boot > Spring Data JPA' 카테고리의 다른 글
querydsl 에서 case문 사용하기 (0) | 2024.01.07 |
---|---|
QueryDsl) BooleanExpression (1) | 2023.12.17 |
queryDsl 정리 및 예제 (1) | 2023.12.03 |
JPA) JPA Auditing으로 자동화 (0) | 2023.12.01 |
QueryDSL) From 절 SubQuery를 쓰고싶어 (2) | 2023.11.23 |