Junhyunny's Devlogs

글 검색

기술 글 제목, 본문, 카테고리에서 검색합니다.

    이전 화면으로

    QueryDSL에서 같은 엔티티를 여러 번 조인하기

    • java
    • jpa
    • query-dsl
    이 글의 목차

    0. 들어가면서

    엔티티(entity)는 여러 가지 역할을 할 수 있다. 그에 따라 하나의 쿼리(query)에서 여러 번 조인(join)이 필요할 수 있다. 이런 케이스를 QueryDSL를 통해 조인 쿼리로 작성하면 정상적인 결과를 얻지 못할 수 있다. 실제 프로젝트의 비즈니스 케이스를 다룰 수는 없으니 간단한 예시를 통해 문제 현상과 해결 방법을 알아보겠다.

    1. Context

    다음과 같은 상황을 구상해봤다.

    • 회사 간의 거래(trade)를 중개하는 시스템이 있다.
    • 회사는 거래에서 제공자(provider)이거나 소비자(consumer)일 수 있다.
    • 시스템은 회사를 구분하는 방법으로 사업자 번호 이 외에 제공자 ID, 소비자 ID를 별도로 만들어 관리한다.
    • 시스템을 통해 거래 정보를 조회할 때 관련된 회사 정보를 함께 보고자 한다.

    2. 문제 현상

    문제가 되는 코드를 살펴보고 실행되는 쿼리를 살펴보겠다.

    2.1. 문제 코드

    • 거래 정보를 조회한다.
    • 제공자 ID, 소비자 ID를 이용해 거래와 제공자, 소비자 사이의 LEFT OUTER JOIN을 수행한다.
    • 회사 엔티티에서 필요한 정보를 함께 조회한다.
    package action.in.blog.join;
    
    import com.querydsl.core.types.Projections;
    import com.querydsl.jpa.impl.JPAQueryFactory;
    import org.springframework.stereotype.Repository;
    
    import javax.persistence.EntityManager;
    import java.util.List;
    
    @Repository
    public class TradeStore {
    
        private final JPAQueryFactory jpaQueryFactory;
    
        public TradeStore(EntityManager entityManager) {
            this.jpaQueryFactory = new JPAQueryFactory(entityManager);
        }
    
        public List<TradeVO> getTradeInformation() {
            QTradeEntity trade = QTradeEntity.tradeEntity;
            QCompanyEntity provider = QCompanyEntity.companyEntity;
            QCompanyEntity consumer = QCompanyEntity.companyEntity;
            return jpaQueryFactory
                    .select(
                            Projections.fields(
                                    TradeVO.class,
                                    trade.id,
                                    trade.providerId,
                                    provider.name.as("providerName"),
                                    trade.consumerId,
                                    consumer.name.as("consumerName"),
                                    trade.basedPrice,
                                    trade.contractSize
                            )
                    )
                    .from(trade)
                    .leftJoin(provider).on(trade.providerId.eq(provider.providerId))
                    .leftJoin(consumer).on(trade.consumerId.eq(consumer.consumerId))
                    .fetch();
        }
    }
    실행 결과
    • 제공자에 대한 LEFT OUTER JOIN은 실행되었다.
    • 소비자에 대한 LEFT OUTER JOIN은 실행되지 않았다.
    select tradeentit0_.id            as col_0_0_,
           tradeentit0_.provider_id   as col_1_0_,
           companyent1_.name          as col_2_0_,
           tradeentit0_.consumer_id   as col_3_0_,
           companyent1_.name          as col_4_0_,
           tradeentit0_.based_price   as col_5_0_,
           tradeentit0_.contract_size as col_6_0_
    from trade_entity tradeentit0_
             left outer join company_entity companyent1_ on (tradeentit0_.provider_id = companyent1_.provider_id)

    3. 문제 해결

    같은 엔티티를 하나의 질의문에서 여러 번 조인하는 경우 각 엔티티를 다른 객체로 구분해줘야 한다. 기본 엔티티를 같이 사용하는 경우 동일한 객체로 판단되어 정상적인 쿼리가 수행되지 않는다.

    3.1. 문제 해결 코드

    • 제공자 객체와 소비자 객체에 대한 식별자를 각자 지정해주어 구분한다.
    • 제공자, 소비자 객체를 사용해 조인 쿼리를 수행한다.
    package action.in.blog.join;
    
    import com.querydsl.core.types.Projections;
    import com.querydsl.jpa.impl.JPAQueryFactory;
    import org.springframework.stereotype.Repository;
    
    import javax.persistence.EntityManager;
    import java.util.List;
    
    @Repository
    public class TradeStore {
    
        private final JPAQueryFactory jpaQueryFactory;
    
        public TradeStore(EntityManager entityManager) {
            this.jpaQueryFactory = new JPAQueryFactory(entityManager);
        }
    
        public List<TradeVO> getTradeInformation() {
            QTradeEntity trade = QTradeEntity.tradeEntity;
            QCompanyEntity provider = new QCompanyEntity("provider");
            QCompanyEntity consumer = new QCompanyEntity("consumer");
            return jpaQueryFactory
                    .select(
                            Projections.fields(
                                    TradeVO.class,
                                    trade.id,
                                    trade.providerId,
                                    provider.name.as("providerName"),
                                    trade.consumerId,
                                    consumer.name.as("consumerName"),
                                    trade.basedPrice,
                                    trade.contractSize
                            )
                    )
                    .from(trade)
                    .leftJoin(provider).on(trade.providerId.eq(provider.providerId))
                    .leftJoin(consumer).on(trade.consumerId.eq(consumer.consumerId))
                    .fetch();
        }
    }
    실행 결과
    • 제공자와 소비자에 대한 LEFT OUTER JOIN이 각각 실행되었다.
    select tradeentit0_.id            as col_0_0_,
           tradeentit0_.provider_id   as col_1_0_,
           companyent1_.name          as col_2_0_,
           tradeentit0_.consumer_id   as col_3_0_,
           companyent2_.name          as col_4_0_,
           tradeentit0_.based_price   as col_5_0_,
           tradeentit0_.contract_size as col_6_0_
    from trade_entity tradeentit0_
             left outer join company_entity companyent1_ on (tradeentit0_.provider_id = companyent1_.provider_id)
             left outer join company_entity companyent2_ on (tradeentit0_.consumer_id = companyent2_.consumer_id)

    3.2. 테스트 코드

    package action.in.blog.join;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
    
    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.EntityTransaction;
    import javax.persistence.PersistenceUnit;
    import java.util.List;
    import java.util.UUID;
    import java.util.function.Consumer;
    
    import static org.hamcrest.MatcherAssert.assertThat;
    import static org.hamcrest.Matchers.equalTo;
    
    @DataJpaTest
    public class TradeStoreIT {
    
        @PersistenceUnit
        EntityManagerFactory factory;
    
        void transaction(Consumer<EntityManager> consumer) {
            EntityManager em = factory.createEntityManager();
            EntityTransaction transaction = em.getTransaction();
            transaction.begin();
            try {
                consumer.accept(em);
            } catch (Exception ex) {
                throw ex;
            } finally {
                transaction.rollback();
                em.close();
            }
        }
    
        void flushAndClear(EntityManager em) {
            em.flush();
            em.clear();
        }
    
        @Test
        void getTradeInformation() {
            transaction(em -> {
                em.persist(
                        CompanyEntity.builder()
                                .bizRegistrationNumber(UUID.randomUUID().toString())
                                .name("(주) Alpha Company")
                                .providerId("0001")
                                .consumerId("5001")
                                .build()
                );
                em.persist(
                        CompanyEntity.builder()
                                .bizRegistrationNumber(UUID.randomUUID().toString())
                                .name("(주) Beta Company")
                                .providerId("0002")
                                .consumerId("5002")
                                .build()
                );
                em.persist(
                        TradeEntity.builder()
                                .providerId("0001")
                                .consumerId("5002")
                                .basedPrice(2500)
                                .contractSize(5)
                                .build()
                );
                flushAndClear(em);
    
    
                TradeStore sut = new TradeStore(em);
                List<TradeVO> result = sut.getTradeInformation();
    
    
                assertThat(result.size(), equalTo(1));
                TradeVO firstTrade = result.get(0);
                assertThat(firstTrade.getProviderId(), equalTo("0001"));
                assertThat(firstTrade.getProviderName(), equalTo("(주) Alpha Company"));
                assertThat(firstTrade.getConsumerId(), equalTo("5002"));
                assertThat(firstTrade.getConsumerName(), equalTo("(주) Beta Company"));
                assertThat(firstTrade.getBasedPrice(), equalTo(2500));
                assertThat(firstTrade.getContractSize(), equalTo(5));
            });
        }
    }

    KEEP READING

    DISCUSSION

    댓글