[JPA] MultipleBagFetchException: cannot simultaneously fetch multiple bags

들어가며

JPA를 이용해서 하나의 Entity에 1:N 관계를 여러개 설정을 해서 사용하다 보면 발생하는 MultipleBagFetchException에 대해서 알아보자.

언제 발생하는가?

@Data
@Entity
public class User {
  @Id
  @GeneratedValue
  private Long id;
  private String name;
  private String country;

  @ElementCollection(fetch = FetchType.LAZY)
  private List<String> nameHistories;

  @ElementCollection(fetch = FetchType.LAZY)
  private List<String> countryHistories;
}
  • User Entity에 nameHistories, countryHistories처럼 1:N 관계가 형성되는 2개의 ElementCollection을 생성하였다.
@DataJpaTest
class UserFetchJoinTest {
  static final QUser qUser = QUser.user;
  @Autowired
  TestEntityManager em;
  private User createUser() {
    User user = new User();
    user.setName("woojin");
    user.setCountry("KR");
    user.setCountryHistories(List.of("KR"));
    user.setNameHistories(List.of("woojin"));
    return em.persist(user);
  }

  @Test
   void elementCollectionSizeIsOne() {
   User user = createUser();
   JPAQuery<User> query = new JPAQuery<>(em.getEntityManager());

   User actual = query.select(qUser)
                      .from(qUser)
                      .leftJoin(qUser.nameHistories).fetchJoin()
                      .leftJoin(qUser.countryHistories).fetchJoin()
                      .where(qUser.id.eq(user.getId()))
                      .fetchOne();
   assertThat(actual.getCountryHistories()).hasSize(1);
   assertThat(actual.getNameHistories()).hasSize(1);
  }
}
  • User를 생성하고, 조회할때 nameHistories, countryHistories를 fetchJoin을 통해서 EAGER로 가져오려고 할때 MultipleBagFetchException이 발생하게 된다.

왜 발생하는가?

  • 조회할때 BagType의 Collection을 2개 이상 조회하려고 했기에 발생하였다.
  • BagType이 무슨말인가? Collection에서 중복을 허용하는 Collection을 Bag이라는 것을 한번쯤은 들어봤을 것이다.
public abstract class BasicLoader extends Loader {
  protected void postInstantiate() {
    Loadable[] persisters = getEntityPersisters();
    String[] suffixes = getSuffixes();
    descriptors = new EntityAliases[persisters.length];
    for ( int i=0; i<descriptors.length; i++ ) {
      descriptors[i] = new DefaultEntityAliases( persisters[i], suffixes[i] );
    }

    CollectionPersister[] collectionPersisters = getCollectionPersisters();
    List bagRoles = null;
    if ( collectionPersisters != null ) {
      String[] collectionSuffixes = getCollectionSuffixes();
      collectionDescriptors = new CollectionAliases[collectionPersisters.length];
      for ( int i = 0; i < collectionPersisters.length; i++ ) {
        if ( isBag( collectionPersisters[i] ) ) {
          if ( bagRoles == null ) {
            bagRoles = new ArrayList();
          }
          bagRoles.add( collectionPersisters[i].getRole() );
        }
        collectionDescriptors[i] = new GeneratedCollectionAliases(collectionPersisters[i],collectionSuffixes[i]);
      }
    }
    else {
      collectionDescriptors = null;
    }
    if ( bagRoles != null && bagRoles.size() > 1 ) {
      throw new MultipleBagFetchException( bagRoles );
    }
  }
}
  • QueryPlan을 생성할 때 동작하는 부분인데, 마지막에 bagRoles.size() > 1 인 경우 MultipleBagFetchException를 발생 한다.
  • 왜 이러한 에러를 발생시켰는지에 대해서는 잘 모르겠지만 추측되는 원인에 대해서 적어보겠다.
  • 1:N, 1:M을 fetchJoin으로 가져오게 되면 query에 left outer join으로 query를 가져오게 되고, 결과가 N*M개의 데이터가 생성이 되어 중복 데이터가 발생하게 된다.
  • List의 경우 중복 데이터를 허용하니, 필요로 했던 데이터 보다 많은 데이터가 생성이 될 것이기에 이러한 문제를 사전에 차단했지 않을까 싶다.

어떻게 해결하는가?

@Data
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private String country;

    @ElementCollection
    private Set<String> nameHistories;

    @ElementCollection
    private Set<String> countryHistories;
}
  • BagType이 발생하지 않게 MapType이나 SetType으로 변경하면 해당 에러가 발생하지 않고 정상적으로 query가 발생하게 된다.

마치며

Leave a comment