2개 이상의 컬렉션 즉시로딩 문제
2개 이상의 컬렉션 즉시로딩 문제
현상
- 2개 이상의 컬렉션 연관관계를 가진 엔티티를 즉시로딩으로 조회시 MultipleBagFetchException 예외가 발생한다.
원인
- JPA 에서 컬렉션을 2개 이상 즉시로딩 할 수 없다.
- Fetch Join도 컬렉션은 1개까지만 가능하다.
예제 코드
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
@OneToMany(mappedBy = "member", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Address> addresses = new ArrayList<>();
public void addOrder(Order order) {
orders.add(order);
order.setMember(this);
}
public void addAddress(Address address) {
addresses.add(address);
address.setMember(this);
}
}
@Entity
@Getter @Setter
@NoArgsConstructor
public class Address {
@Id @GeneratedValue
@Column(name = "address_id")
private Long id;
private String city;
private String street;
private String zipcode;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
private int price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Order order1 = new Order();
Order order2 = new Order();
Order order3 = new Order();
Order order4 = new Order();
Address address1 = new Address("city1", "street1", "zipcode1");
Address address2 = new Address("city2", "street2", "zipcode2");
Address address3 = new Address("city3", "street3", "zipcode3");
Address address4 = new Address("city4", "street4", "zipcode4");
Address address5 = new Address("city5", "street5", "zipcode5");
Address address6 = new Address("city6", "street6", "zipcode6");
Member member1 = new Member();
member1.setName("member1");
member1.addOrder(order1);
member1.addOrder(order2);
member1.addAddress(address1);
member1.addAddress(address2);
member1.addAddress(address3);
Member member2 = new Member();
member2.setName("member2");
member2.addOrder(order3);
member2.addOrder(order4);
member2.addAddress(address4);
member2.addAddress(address5);
member2.addAddress(address6);
em.persist(member1);
em.persist(member2);
em.flush();
em.clear();
tx.commit();
}
catch (Exception e) {
tx.rollback();
e.printStackTrace();
}
finally {
em.close();
}
emf.close();
}
}
-
데이터베이스에서 조인시 결과 테이블의 행은 ADDRESS테이블의 행 m과 ORDERS테이블의 행 n의 곱인 m * n 만큼 발생한다.
-
2개 이상의 컬렉션 연관관계를 가진 엔티티에서 전체 데이터를 효율적으로 가져오려면 어떻게 해야할까?
- 모든 컬렉션을 Lazy Loading
- 컬렉션 하나에만 Fetch Join, 나머지 컬렉션은 Lazy Loading
- 컬렉션 자료구조를 List가 아닌 Set로 설정
- Hibernate default_batch_fetch_size
1. 모든 컬렉션을 Lazy Loading
- N + 1 문제가 발생한다.
- Member 쿼리 하나로 컬렉션 개수 만큼 쿼리가 더 발생한다.
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
for (Member member : members) {
for (Address address : member.getAddresses()) {
System.out.println("address = " + address);
}
for (Order order : member.getOrders()) {
System.out.println("order = " + order);
}
}
select m.member_id, m.name
from Member m
select a.member_id, a.address_id, a.address_id, a.city, a.member_id, a.street, a.zipcode
from Address a
where a.member_id=?
select o.member_id, o.order_id, o.order_id, o.member_id, o.price
from orders o
where o.member_id=?
select a.member_id, a.address_id, a.address_id, a.city, a.member_id, a.street, a.zipcode
from Address a
where a.member_id=?
select o.member_id, o.order_id, o.order_id, o.member_id, o.price
from orders o
where o.member_id=?
2. 컬렉션 하나에만 Fetch Join, 나머지 컬렉션은 Lazy Loading
- 컬렉션 개수가 더 많은 쪽을 Fetch Join 하고 나머지 컬렉션은 지연 로딩을 사용한다.
- 나머지 컬렉션 개수 만큼 쿼리가 발생한다.
List<Member> members = em.createQuery("select distinct m from Member m left join fetch m.addresses", Member.class).getResultList();
for (Member member : members) {
for (Address address : member.getAddresses()) {
System.out.println("address = " + address);
}
for (Order order : member.getOrders()) {
System.out.println("order = " + order);
}
}
select distinct m.member_id, a.address_id, m.name, a.city, a.member_id, a.street, a.zipcode, a.member_id, a.address_id
from Member m left outer join Address a
on m.member_id=a.member_id
select o.member_id, o.order_id, o.order_id, o.member_id, o.price
from orders o
where o.member_id=?
select o.member_id, o.order_id, o.order_id, o.member_id, o.price
from orders o
where o.member_id=?
3. 컬렉션 자료구조를 List가 아닌 Set로 설정
- 컬렉션 자료구조를 Set으로 정의한다.
- Join 한방 쿼리가 나간다.
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Order> orders = new LinkedHashSet<>();
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Address> addresses = new LinkedHashSet<>();
public void addOrder(Order order) {
orders.add(order);
order.setMember(this);
}
public void addAddress(Address address) {
addresses.add(address);
address.setMember(this);
}
}
List<Member> members = em.createQuery("select distinct m from Member m left join fetch m.addresses left join fetch m.orders", Member.class).getResultList();
for (Member member : members) {
for (Address address : member.getAddresses()) {
System.out.println("address = " + address);
}
for (Order order : member.getOrders()) {
System.out.println("order = " + order);
}
}
select
distinct m.member_id, a.address_id, o.order_id, m.name, a.city, a.member_id, a.street, a.zipcode, a.member_id, a.address_id, o.member_id, o.price, o.member_id, o.order_id
from Member m
left outer join Address a on m.member_id=a.member_id
left outer join orders o on m.member_id=o.member_id
4. Hibernate default_batch_fetch_size, @org.hibernate.annotations.BatchSize
- 컬렉션 개수가 더 많은 쪽을 Fetch Join 하고 나머지 컬렉션은 지연 로딩을 사용한다.
- 나머지 컬렉션은 batch_fetch_size 사이즈 만큼 한방 쿼리로 나간다.
- default_batch_fetch_size는 애플리케이션 전체 설정되고, @BatchSize는 해당 필드만 적용된다.
<!-- persistence.xml -->
<property name="hibernate.default_batch_fetch_size" value="10"/>
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@org.hibernate.annotations.BatchSize(size = 10)
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Address> addresses = new LinkedHashSet<>();
public void addOrder(Order order) {
orders.add(order);
order.setMember(this);
}
public void addAddress(Address address) {
addresses.add(address);
address.setMember(this);
}
}
List<Member> members = em.createQuery("select distinct m from Member m left join fetch m.addresses", Member.class).getResultList();
for (Member member : members) {
for (Address address : member.getAddresses()) {
System.out.println("address = " + address);
}
for (Order order : member.getOrders()) {
System.out.println("order = " + order);
}
}
select distinct m.member_id, a.address_id, m.name, a.city, a.member_id, a.street, a.zipcode, a.member_id, a.address_id
from Member m
left outer join Address a on m.member_id=a.member_id
select o.member_id, o.order_id, o.order_id, o.member_id, o.price
from orders o
where o.member_id in (?, ?)
Leave a comment