Spring Data JPA에서의 Projection 방법

박진형·2022년 2월 13일
1

JPA

목록 보기
7/7

Projection

DB의 필요한 속성만을 조회하는 것을 projection이라고 한다.
Spring Data JPA에서 projection을 하는 방법을 알아보자.

아래와 같이 원하는 데이터를 Select하고 그에 맞는 자료형으로 반환을 받으면 될것 같았지만 작동하지 않았다. 별도의 방법이 있을거라 생각하고 알아봤다.

@Query("SELECT s.id as id FROM Stadium s JOIN s.company c WHERE c.id=:companyId")
    List<Long> findAllIdsByCompanyId(@Param("companyId") Long companyId);

인터페이스 기반 Projection

조회를 원하는 속성들의 집합으로 인터페이스를 만든다. get(필드명)으로 메소드를 만들면된다.
나는 id만을 조회하기를 원하므로 getId()로 만든다. 필드명의 맨 앞은 대문자로 적어준다.

interface StadiumId{
        Long getId();
    }

이렇게 인터페이스를 작성했다면 아까 적은 잘못된 쿼리를 아래와 같이 수정 해준다.
주의할 점은 s.id의 별칭을 별도로 필드명과 똑같이 설정 해주어야한다. 그렇게 해야만 매핑이 잘 되는 듯하다.

Closed Projection

닫힌 프로젝션은 지정한 속성만을 프로젝션한다.
가져오려는 속성이 무엇인지 먼저 파악이 가능하므로 쿼리 최적화가 가능하다.

@Query("SELECT s.id as id FROM Stadium s JOIN s.company c WHERE c.id=:companyId")
    List<StadiumId> findAllIdsByCompanyId(@Param("companyId") Long companyId);

쿼리 메소드도 가능하지만 나는 모든 id를 뽑아내기 원했으므로 내가 원하는 기능은 아니었다.

List<StadiumId> findProjectionsById(Long id);

Spring Data JPA 참조 문서에 따르면 Projection은 재귀적으로 사용할 수 있다고 한다.
문서의 예제에 아래와 같이 나와있다. 딱 봐도 무엇을 의미하는지 알 수 있다.

interface PersonSummary {

  String getFirstname();
  String getLastname();
  AddressSummary getAddress();

  interface AddressSummary {
    String getCity();
  }
}

Open Projection

아래와 같이 @Value 어노테이션을 이용해 열린 프로젝션을 할 수 있다.
target으로 지정된 속성을 조합해 새로운 반환 값으로 커스터마이징 할 수 있다.
@Value 어노테이션에 들어가는 표현식을 SpEL이라고 하는데 SpEL 표현식이 Entity의 모든 속성을 사용할 수 있기 때문에 쿼리 실행 최적화를 적용할 수 없다고 한다.

 @Query("SELECT s.id as id , s.type as type FROM Stadium s JOIN s.company c WHERE c.id=:companyId")
    List<StadiumIdAndType> findProjectionIdAndTypeByCompanyId(@Param("companyId") Long companyId);
interface StadiumIdAndType{
        @Value("#{target.id + ' '+ target.type}")
        String getIdAndType();
    }

출력 해보면 아래와 같다

   for (StadiumIdAndType stadium : result) {
      log.info("stadium = {}",stadium.getIdAndType());
    }

프로젝트 적용

사실 id만 뽑아서 다른 쿼리의 IN절에 바로 넣어 사용할 생각이었지만 IN절은 아무리 인터페이스 안에 있는 데이터가 Long형 이라도 매핑을 못해주는 것 같다.

  • 불가능
@Query(
      "SELECT new com.deu.football_love.dto.match.QueryMatchDto(m.id, t.name, m.stadium.id, m.approval, m.reservationTime) "
          + "FROM Matches m "
          + "JOIN m.stadium s "
          + "JOIN m.team t "
          + "WHERE s.id IN(:stadiums)")
  List<QueryMatchDto> findAllByStadiumIdList(@Param("stadiums") List<Stadium> stadiums);

프로젝션한 결과를 map()함수를 이용해서 Long형 리스트로 변경해 준다.

 List<Long> stadiumIdList = stadiumRepository.findAllIdsByCompanyId(companyId).stream().map(id->id.getId()).collect(
        Collectors.toList());
@Query(
      "SELECT new com.deu.football_love.dto.match.QueryMatchDto(m.id, t.name, m.stadium.id, m.approval, m.reservationTime) "
          + "FROM Matches m "
          + "JOIN m.stadium s "
          + "JOIN m.team t "
          + "WHERE s.id IN(:stadiums)")
  List<QueryMatchDto> findAllByStadiumIdList(@Param("stadiums") List<Long> stadiums);

아무래도 전체 엔티티를 가져와서 map을 돌리는 것보다 비교적 작은 데이터를 map 하는 것이 더 성능상 조금이라도 이점이 있지 않을까 싶다.

정리

  • @Query 어노테이션 사용 시 as로 별칭 부여 꼭해야함
  • get필드명(맨 앞글자는 대문자)로 getter를 만들어 줘야함
  • Nested Projection 가능
  • Closed Projection
    • 쿼리 최적화 가능
  • Open Projection
    • 모두 가져와서 원하는 것을 조합
    • 쿼리 최적화 불가능

1개의 댓글

comment-user-thumbnail
2024년 4월 16일

좋은 내용 잘 보고 갑니다!

답글 달기