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 (?, ?)

Tags:

Categories:

Updated:

Leave a comment