본문 바로가기

Spring/JPA

[Spring Data JPA] @Query

이번 장에서는 @Query 어노테이션을 사용한 조회에 대해서 알아본다.
글의 하단부에 참고한 강의와 공식문서의 경로를 첨부하였으므로 자세한 사항은 강의나 공식문서에서 확인한다.
모든 코드는 깃허브 (링크)에 있다.


NamedQuery가 Entity에 존재하였다면 @Query를 사용하여 직접 쿼리를 작성하는 방식은 @NamedQuery의 내용이 @Query 어노테이션 안으로 들어온 방식이다.
SoccerPlayer가 가지는 속성은 아래와 같으며 Entity가 아닌 DTO로 조회하기위해 ResponseDTO를 추가하였다.

SoccerPlayer

@Entity
@NamedQueries(value = {
        @NamedQuery(
                name = "SoccerPlayer.findByName",
                query = "SELECT SP FROM SoccerPlayer SP WHERE SP.name = :name"),
        @NamedQuery(
                name = "SoccerPlayer.findByHeightGreaterThan",
                query = "SELECT SP FROM SoccerPlayer SP WHERE SP.height > :height")
})
@Getter @Setter
@ToString(of = {"id", "name", "height", "weight"})
@NoArgsConstructor(access = PROTECTED)
public class SoccerPlayer {

    @Id
    @GeneratedValue
    @Column(name = "soccer_player_id")
    private Long id;
    private String name;
    private int height;
    private int weight;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    @Getter
    @AllArgsConstructor
    public static class ResponseDTO {
        private String name;
        private int height;
        private int weight;
    }

    // 이하 생략
}

SoccerPlayerDataRepository

public interface SoccerPlayerDataRepository extends JpaRepository<SoccerPlayer, Long> {
    @Query(value = "SELECT SP FROM SoccerPlayer SP")
    List<SoccerPlayer> findEntityAllUsingQueryAnnotation();

    @Query(value =
            "SELECT new com.roy.datajpa.domain.SoccerPlayer.ResponseDTO " +
            "(SP.name, SP.height, SP.weight) " +
            "FROM SoccerPlayer SP")
    List<SoccerPlayer.ResponseDTO> findDTOAllUsingQueryAnnotation();
}

SoccerPlayerDataRepositoryTest

@Transactional
@SpringBootTest
class SoccerPlayerDataRepositoryTest {

    @Autowired
    private SoccerPlayerDataRepository dataRepository;
    @Test
    @DisplayName("@Query Entity 조회 테스트")
    void queryAnnotationFindEntityTest() {
        List<SoccerPlayer> players = List.of(
                new SoccerPlayer("Roy", 173),
                new SoccerPlayer("Perry", 183)
        );
        dataRepository.saveAll(players);

        List<SoccerPlayer> result =
                dataRepository.findEntityAllUsingQueryAnnotation();
        assertEquals(2, result.size());
    }

    @Test
    @DisplayName("@Query DTO 조회 테스트")
    void queryAnnotationFindDTOTest() {
        List<SoccerPlayer> players = List.of(
                new SoccerPlayer("Roy", 173),
                new SoccerPlayer("Perry", 183)
        );
        dataRepository.saveAll(players);

        List<SoccerPlayer.ResponseDTO> result =
                dataRepository.findDTOAllUsingQueryAnnotation();
        assertEquals(2, result.size());
        assertEquals(players.get(0).getName(), result.get(0).getName());
        assertEquals(players.get(0).getHeight(), result.get(0).getHeight());
    }
}

테스트를 진행해보면 아래와 같은 오류가 발생하며 실패한다.
사실 글을 쓰면서 예상하지 못한 일인데 JPA의 리턴 타입으로 Inner Class를 사용할 수 없다.

원인은 추후에 생각해보고 DTO 클래스를 Entity 밖으로 꺼내서 다시 진행해본다.
수정된 코드들은 아래와 같다.

Entity & DTO

@Entity
@NamedQueries(value = {
        @NamedQuery(
                name = "SoccerPlayer.findByName",
                query = "SELECT SP FROM SoccerPlayer SP WHERE SP.name = :name"),
        @NamedQuery(
                name = "SoccerPlayer.findByHeightGreaterThan",
                query = "SELECT SP FROM SoccerPlayer SP WHERE SP.height > :height")
})
@Getter @Setter
@ToString(of = {"id", "name", "height", "weight"})
@NoArgsConstructor(access = PROTECTED)
public class SoccerPlayer {

    @Id
    @GeneratedValue
    @Column(name = "soccer_player_id")
    private Long id;
    private String name;
    private int height;
    private int weight;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}

@Getter
@AllArgsConstructor
public class SoccerPlayerResponseDTO {
    private String name;
    private int height;
    private int weight;
}

Repository

public interface SoccerPlayerDataRepository extends JpaRepository<SoccerPlayer, Long> {
    @Query(value = "SELECT SP FROM SoccerPlayer SP")
    List<SoccerPlayer> findEntityAllUsingQueryAnnotation();

    @Query(value =
            "SELECT new com.roy.datajpa.repository.data.query.dto.SoccerPlayerResponseDTO " +
            "(SP.name, SP.height, SP.weight) " +
            "FROM SoccerPlayer SP")
    List<SoccerPlayerResponseDTO> findDTOAllUsingQueryAnnotation();
}

Test

@Transactional
@SpringBootTest
class SoccerPlayerDataRepositoryTest {
    @Test
    @DisplayName("@Query Entity 조회 테스트")
    void queryAnnotationFindEntityTest() {
        List<SoccerPlayer> players = List.of(
                new SoccerPlayer("Roy", 173),
                new SoccerPlayer("Perry", 183)
        );
        dataRepository.saveAll(players);

        List<SoccerPlayer> result =
                dataRepository.findEntityAllUsingQueryAnnotation();
        assertEquals(2, result.size());
    }

    @Test
    @DisplayName("@Query DTO 조회 테스트")
    void queryAnnotationFindDTOTest() {
        List<SoccerPlayer> players = List.of(
                new SoccerPlayer("Roy", 173),
                new SoccerPlayer("Perry", 183)
        );
        dataRepository.saveAll(players);

        List<SoccerPlayerResponseDTO> result =
                dataRepository.findDTOAllUsingQueryAnnotation();
        assertEquals(2, result.size());
        assertEquals(players.get(0).getName(), result.get(0).getName());
        assertEquals(players.get(0).getHeight(), result.get(0).getHeight());
    }
}

이번에는 정상적으로 테스트를 통과하는 것을 확인할 수 있다.


내부 클래스는 왜 JPA에서 조회하지 못할까.
어디까지나 정답이 아닌 필자의 추측일 뿐이다.

data-mongodb에서는 @Document 어노테이션(jpa의 @Entity)이 붙은 클래스의 내부 클래스에 DTO를 생성하고 조회까지 가능하였다.
그렇다면 data-mongodb와 data-jpa는 어떠한 차이가 있을까?

필자가 생각하는 가장 큰 차이는 영속성 컨텍스트의 존재가 아닐까 싶다.
data-mongodb는 Transaction, 지연 로딩의 개념이 없기 때문에 프록시 객체를 사용하지 않았다.
하지만 data-jpa는 Transaction, 지연 로딩을 위해 프록시를 사용해야하는데 이러한 프록시 기술를 사용하기 위한 클래스로
내부 클래스는 사용할 수 없는 것 아닐까 라고 조심스럽게 예상해본다. (아직 갈 길이 멀다...)


참고한 강의:

JPA 공식 문서:

위키백과:

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

[Spring Data JPA] Paging  (0) 2022.03.25
[Spring Data JPA] Parameter binding  (0) 2022.03.25
[Spring Data JPA] Named Query  (0) 2022.03.25
[Spring Data JPA] Method name query  (0) 2022.03.25
[Spring Data JPA] Structure  (0) 2022.03.25