본문 바로가기

Spring/JPA

[Querydsl] Join

이번 장에서는 Querydsl의 조인에 대해서 알아본다.
글의 하단부에 참고한 강의와 공식문서의 경로를 첨부하였으므로 자세한 사항은 강의나 공식문서에서 확인한다.
모든 코드는 깃허브 (링크)에 올려두었다.


Default Join

첫 번째 파라미터로 조인 대상을 지정한다.
두 번째 파라미터로 별칭(alias)로 사용할 Q 타입을 지정한다.

이번에는 Q 클래스 파일에 기본으로 있는 static 인스턴스를 사용하였다.
만약 같은 테이블에 두번의 조인이 필요하다면 두 개의 별칭(alias)이 필요하게 되고
new QSoccerPlayer("alias") 와 같이 새로운 별칭을 생성하여 사용해야한다.

@Transactional
@SpringBootTest
@TestMethodOrder(value = OrderAnnotation.class)
public class QuerydslBasicGrammarTest {
    @Autowired
    private EntityManager entityManager;
    private JPAQueryFactory query;
    @Test
    @Order(16)
    @DisplayName("기본 조인 테스트")
    void defaultJoinTest() {
        // Inner Join
        List<SoccerPlayer> storedPlayers = query
                .selectFrom(soccerPlayer)
                .join(soccerPlayer.team, team)
                .where(team.name.eq("TeamA"))
                .fetch();

        storedPlayers.forEach(player ->
                assertTrue(player.getName().equals("Roy")
                        || player.getName().equals("Perry"))
        );

        assertDoesNotThrow(() -> {
            // Inner Join
            query
                    .selectFrom(soccerPlayer)
                    .innerJoin(soccerPlayer.team, team)
                    .fetch();

            // Left Outer Join
            query
                    .selectFrom(soccerPlayer)
                    .leftJoin(soccerPlayer.team, team)
                    .fetch();

            // Right Outer Join
            query
                    .selectFrom(soccerPlayer)
                    .rightJoin(soccerPlayer.team, team)
                    .fetch();
        });
    }
}

Theta Join

연관 관계가 없는 필드에 조인한다.
from 절에 여러 엔티티를 입력한다.

@Transactional
@SpringBootTest
@TestMethodOrder(value = OrderAnnotation.class)
public class QuerydslBasicGrammarTest {
    @Autowired
    private EntityManager entityManager;
    private JPAQueryFactory query;
    @Test
    @Order(17)
    @DisplayName("세타 조인 테스트")
    void thetaJoinTest() {
        entityManager.persist(new SoccerPlayer("TeamA"));

        List<SoccerPlayer> storedPlayers = query
                .select(soccerPlayer)
                .from(soccerPlayer, team)
                .where(soccerPlayer.name.eq(team.name))
                .fetch();

        storedPlayers.forEach(player -> assertEquals(player.getName(), "TeamA"));
    }
}

조인 조건을 Where절에 적는 경우 Outer Join이 불가능하다.
Outer Join을 하기위해서는 On을 사용해야한다.


On

조인 대상을 필터링하며 연관관계가 없는 엔티티를 외부 조인할 때 사용된다.

조인 대상을 필터링 하는 용도로 사용

@Transactional
@SpringBootTest
@TestMethodOrder(value = OrderAnnotation.class)
public class QuerydslBasicGrammarTest {
    @Autowired
    private EntityManager entityManager;
    private JPAQueryFactory query;
    @Test
    @Order(18)
    @DisplayName("조인 ON 테스트")
    void joinOnTest() {
        assertDoesNotThrow(() -> {
            List<Tuple> result = query
                    .select(soccerPlayer, team)
                    .from(soccerPlayer)
                    .leftJoin(soccerPlayer.team, team).on(team.name.eq("TeamA"))
                    .fetch();

            result.forEach(tuple -> System.out.println("tuple = " + tuple));
        });
    }
}

출력 결과는 아래와 같다.

tuple = [SoccerPlayer(id=1, name=Roy, height=173, weight=73), Team(id=2, name=TeamA)]
tuple = [SoccerPlayer(id=3, name=Perry, height=175, weight=75), Team(id=2, name=TeamA)]
tuple = [SoccerPlayer(id=4, name=Sally, height=160, weight=60), null]
tuple = [SoccerPlayer(id=6, name=Dice, height=183, weight=83), null]

Outer Join을 사용할 때 필터링을 사용하려면 On을 사용하면 된다.
Inner Join의 경우 On을 사용하는 것과 Where을 사용하는 것이 동일하게 작동한다.

조인 조건을 주기위한 용도로 사용

@Transactional
@SpringBootTest
@TestMethodOrder(value = OrderAnnotation.class)
public class QuerydslBasicGrammarTest {
    @Autowired
    private EntityManager entityManager;
    private JPAQueryFactory query;
    @Test
    @Order(19)
    @DisplayName("조인 ON 조건 테스트")
    void joinOnConditionTest() {
        entityManager.persist(new SoccerPlayer("TeamA"));
        assertDoesNotThrow(() -> {
            List<Tuple> result = query
                    .select(soccerPlayer, team)
                    .from(soccerPlayer)
                    .leftJoin(team).on(soccerPlayer.name.eq(team.name))
                    .fetch();
            result.forEach(tuple -> System.out.println("tuple = " + tuple));
        });
    }
}

출력 결과는 아래와 같다

tuple = [SoccerPlayer(id=1, name=Roy, height=173, weight=73), null]
tuple = [SoccerPlayer(id=3, name=Perry, height=175, weight=75), null]
tuple = [SoccerPlayer(id=4, name=Sally, height=160, weight=60), null]
tuple = [SoccerPlayer(id=6, name=Dice, height=183, weight=83), null]
tuple = [SoccerPlayer(id=7, name=TeamA, height=0, weight=0), Team(id=2, name=TeamA)]

Fetch Join

페치 조인은 지금까지 공부해왔던 것과 같이 DB에서 제공하는 기능이 아니다.
연관된 Entity를 한 번에 조회하는 JPA의 기능이다.

페치 조인을 사용하지 않는 경우 조회된 SoccerPlayer의 Team은 초기화 되지 않은 것을 알 수 있다.

@Transactional
@SpringBootTest
@TestMethodOrder(value = OrderAnnotation.class)
public class QuerydslBasicGrammarTest {
    @Autowired
    private EntityManager entityManager;
    private JPAQueryFactory query;
    @Test
    @Order(20)
    @DisplayName("페치 조인 미적용 테스트")
    void notAppliedFetchJoinTest() {
        flushAndClear();
        SoccerPlayer storedPlayer = query
                .selectFrom(soccerPlayer)
                .where(soccerPlayer.name.eq("Roy"))
                .fetchOne();

        assertNotNull(storedPlayer.getTeam());
        assertFalse(Hibernate.isInitialized(storedPlayer.getTeam()));
    }
}

페치 조인을 사용하는 경우 일반 조인 뒤에 fetchJoin()만 추가하면 된다.
페치 조인을 사용해서 조회하는 경우 SoccerPlayer의 Team이 초기화 되어 있는 것을 확인할 수 있다.

@Transactional
@SpringBootTest
@TestMethodOrder(value = OrderAnnotation.class)
public class QuerydslBasicGrammarTest {
    @Autowired
    private EntityManager entityManager;
    private JPAQueryFactory query;
    @Test
    @Order(21)
    @DisplayName("페치 조인 적용 테스트")
    void appliedFetchJoinTest() {
        flushAndClear();
        SoccerPlayer storedPlayer = query
                .selectFrom(soccerPlayer)
                .join(soccerPlayer.team, team).fetchJoin()
                .where(soccerPlayer.name.eq("Roy"))
                .fetchOne();

        assertNotNull(storedPlayer.getTeam());
        assertTrue(Hibernate.isInitialized(storedPlayer.getTeam()));
    }
}

참고한 강의:

JPA 공식 문서: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#reference

위키백과: https://ko.wikipedia.org/wiki/%EC%9E%90%EB%B0%94_%ED%8D%BC%EC%8B%9C%EC%8A%A4%ED%84%B4%EC%8A%A4_API

'Spring > JPA' 카테고리의 다른 글

[Querydsl] Projection  (0) 2022.03.30
[Querydsl] Sub Query  (0) 2022.03.30
[Querydsl] Basic Grammar  (0) 2022.03.29
[Querydsl] Initial setting  (0) 2022.03.29
[Spring Data JPA] Native Query  (0) 2022.03.28