JPA

QueryDSL 조인 사용 방법

빈코 2022. 11. 1. 19:31

이전 포스팅에서는 Query DSL을 사용하여 정렬하는 방법과 페이징 처리 등을 포스팅하였습니다. 오늘은 기본 조인 방법부터 fetch 조인까지 알아보려 합니다😄

 

로고
Query DSL

 

개요

예시 관계도

이전 포스팅에서 사용했던 엔티티들을 재사용할 예정입니다. 혹여 엔티티 클래스의 코드가 궁금하신 분들은 엔티티 설계 포스팅을 참고해주세요😄

 

📌 저는 BeforeEach 어노테이션을 활용해 가데이터가 들어간 상황에서 테스트를 진행한 것입니다.

 

@BeforeEach
public void before() {
    queryFactory = new JPAQueryFactory(em);
    Team teamA = new Team("teamA");
    Team teamB = new Team("teamB");
    em.persist(teamA);
    em.persist(teamB);

    Member member1 = new Member("member1", 10, teamA);
    Member member2 = new Member("member2", 20, teamA);

    Member member3 = new Member("member3", 30, teamB);
    Member member4 = new Member("member4", 40, teamB);
    em.persist(member1);
    em.persist(member2);
    em.persist(member3);
    em.persist(member4);
}
MEMBER_ID NAME AGE TEAM_NAME
1 member1 10 teamA
2 member2 20 teamA
3 member3 30 teamB
4 member4 40 teamB

 

기본 조인(Join)📘

조인의 기본 문법은 첫 번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 됩니다.

join(조인 대상, 별칭으로 사용할 Q타입)

 

📌테스트는 teamA에 해당하는 멤버들을 추출하는 테스트 코드를 작성하고자 합니다.

@Test
public void join() {
    List<Member> result = queryFactory
            .selectFrom(member)
            .join(member.team, team)
            .where(team.name.eq("teamA"))
            .fetch();

    assertThat(result)
            .extracting("username")
            .containsExactly("member1", "member2");
}

 

코드를 살펴보면 member 엔티티를 조회하면서 동시에 member.team 즉 team 엔티티와 조인하여 데이터를 가져옵니다. 조건문 where절에는 team(member.team)의 이름이 "teamA"인 데이터만 추출하게 작성하였습니다.

 

List로 전달받은 member entity들 중에서 username 필드로 추출(extracting)하고 예상 값(member1, member2)이 있는지  containsExactly 메서드로 확인합니다.

 

테스트 결과
테스트 성공!

테스트는 성공하는 것을 알 수 있고, DB에 전달된 SQL문 또한 원하는 조건이 모두 들어가 있는 것을 확인할 수 있습니다😀

 

📌기본 SQL 조인 문법

  • join(), innerJoin() : 내부 조인
  • leftJoin() : left 외부 조인(left outer join)
  • rightJoin() : right 외부 조인(right outer join)

 

On절 사용📔

만약 클라이언트 요구사항이 회원은 모두 조회하면서 회원이 소속된 팀 이름이 teamA인 회원만 팀까지 표시되게 해달라고 하면 어떻게 해야 할까요? 먼저 SQL문을 살펴볼게요.

 

SELECT m.*, t.* FROM MEMBER m LEFT JOIN TEAM t 
ON m.TEAM_ID=t.id and t.name='teamA'

 

LEFT OUTER JOIN을 사용한 이유는 요구사항 중에 회원은 모두 조회해야 하기 때문입니다. 그리고 조인할 시에 테이블 간의 team_id가 같아야 하며 팀의 이름이 'teamA'인 것으로 조회하였습니다. 사실 SQL문만 알면 Query DSL로도 쉽게 작성할 수 있습니다.

 

@Test
public void join_on_filtering() {
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team).on(team.name.eq("teamA"))
            .fetch();

    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
    }
}

 

Query DSL 문법이 SQL문과 상당히 비슷하고 직관적이기 때문에 설명은 스킵할게요😅

 

테스트 결과
테스트 성공!

 

📌참고

on 절을 활용해 조인 대상을 필터링 할 때, 외부 조인이 아니라 내부 조인(inner join)을 사용하면, where 절에서 필터링하는 것과 기능이 동일합니다. 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익숙한 where 절로 해결하고, 위 테스트처럼 외부 조인이 필요한 경우에만 기능을 사용하는 것이 좋습니다.

 

패치 조인(Fetch Join)📗

Fetch Join은 일반 Join과 다르게 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화 시킵니다. 흔히 N+1 문제를 해결하는 방법으로 알려져 있는데, 혹여나 Fetch Type(Lazy, Eager)을 모르시면 꼭 해당 링크를 클릭해서 선수 학습해주세요😊 

 

저는 엔티티 설계를 하였을 때 밑에 사진과 같이 다대일 관계는 모두 Fetch Type을 Lazy로 설정하였습니다.

Member Entity
Member Entity

 

Team 필드를 Lazy로 설정했다는 것은 한마디로 member.getTeam()을 호출하지 않는 이상, Team을 조회하는 SELECT문이 DB에 전달되지 않는다는 얘기입니다. 한번 테스트해볼까요?

 

@PersistenceUnit
EntityManagerFactory emf;
@Test
public void fetchJoinNo() {
    em.flush();
    em.clear();

    Member findMember = queryFactory
            .selectFrom(member)
            .where(member.username.eq("member1"))
            .fetchOne();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("패치 조인 미적용").isFalse();
}

 

EntityManagerFactory에서 제공하는 getPersistenceUnitUtil().isLoaded() 메서드는 해당 엔티티가 로딩이 되어있는지 확인시켜주는 기능입니다. 위 테스트 코드는 member1이라는 이름을 가진 유저를 찾고 그 유저로 getTeam()을 호출하였을 때 로딩이 되었는지 테스트해보는 코드입니다.

 

테스트 결과
테스트 결과

테스트 결과가 성공한 걸 보니 team 필드는 가져오지 않았다는 결과입니다😀


그럼 Fetch Join을 사용해볼까요?

 

@Test
public void fetchJoinUse() {
    em.flush();
    em.clear();

    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin()
            .where(member.username.eq("member1"))
            .fetchOne();

    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
    assertThat(loaded).as("패치 조인 적용").isTrue();
}

 

Fetch Join은 일반 Join 문법과 동일하게 작성하고 .fetchJoin()을 붙여주시면 됩니다. 

 

테스트 결과
테스트 결과

테스트는 성공하는 것을 알 수 있었고, SQL문 또한 Team Entity와 inner join을 하여 해당 유저의 team 필드를 가져오는 것을 확인할 수 있었습니다.

 

사실 엔티티의 연관관계와 패치 타입을 모른다면 패치 조인 또한 이해하기 어렵습니다. 중요한 부분이니 꼭 이해하고 넘어가시길 바랄게요😄

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

 

마치며

지금까지 Query DSL을 사용하여 조인하는 방법on절, Fetch Join을 알아보았습니다. 다음 포스팅에는 프로젝션 매핑(Project Mapping)을 소개하는 글로 찾아뵐게요😀

 

QueryDSL Series

1. QueryDSL 적용 및 예제 시리즈 시작

2. QueryDSL 조회 예제

3. QueryDSL 정렬 & 페이징 & 집합

4. QueryDSL 조인 예제(현재)

5. QueryDSL Projection

6. QueryDSL 동적 쿼리 & Bulk 연산

7. QueryDSL 실무 활용

 

Reference

실전! Querydsl

 

반응형