Web/JPA

[Data JPA] EP 6. 나머지 기능들

목차 (클릭시 해당 목차로 이동)


     

     

     

    Projections

     

    • close projection : 가져오고 싶은 부분만 딱 맞춰서 쿼리날려서 가져오기
    • open projection : 일단 엔티티를 다 가져와서 애플리케이션에서 골라내기

     

    인터페이스 기반의 close projection

     

    만약 전체 엔티티가 아니라 회원 이름만 딱 조회하고 싶으면 보통 DTO를 사용합니다.

     

    projections는 엔티티 대신에 DTO를 조회할때 사용하는 기능입니다.

     

    인터페이스만 정의하면 실제 구현체는 스프링 데이터 jpa가 프록시 기술을 이용해 만들어줍니다.

     

    package study.datajpa.repository;
    
    import org.springframework.beans.factory.annotation.Value;
    
    public interface UsernameOnly {
    
        String getUsername();
    }
    

     

    MemberRepository

    List<UsernameOnly> findProjectionsByUsername(@Param("username") String username);

     

     

     

    이제 아래 테스트를 돌리면

        @Test
        public void projections() throws Exception {
            //given
            Team teamA = new Team("teamA");
            em.persist(teamA);
    
            Member m1 = new Member("m1", 0, teamA);
            Member m2 = new Member("m2", 0, teamA);
            em.persist(m1);
            em.persist(m2);
    
            em.flush();
            em.clear();
    
    
            //when
            List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");
    
            for (UsernameOnly usernameOnly : result) {
                System.out.println("usernameOnly = " + usernameOnly.getUsername());
            }
            //then
        }

     

     

    원하는 부분(Username)만 가져오는 쿼리가 나갑니다.

     

     

     

    UsernameOnly에는 프록시가 담겨있습니다.

    프록시로 구현체를 만들어 넣습니다.

     

     

     

    인터페이스 기반의 open projection

     

    @Value를 이용해 인터페이스에 spl문법을 적습니다.

     

    그러면 open projection이 동작해서 엔티티를 다 가져와 애플리케이션에서 spl문법에서 target에 해당되는 부분을 변수에 넣어줍니다.

    package study.datajpa.repository;
    
    import org.springframework.beans.factory.annotation.Value;
    
    public interface UsernameOnly {
    
        @Value("#{target.username + ' ' + target.age}")
        String getUsername();
    }
    

     

     

     

    동일 테스트를 진행하면,

        @Test
        public void projections() throws Exception {
            //given
            Team teamA = new Team("teamA");
            em.persist(teamA);
    
            Member m1 = new Member("m1", 0, teamA);
            Member m2 = new Member("m2", 0, teamA);
            em.persist(m1);
            em.persist(m2);
    
            em.flush();
            em.clear();
    
    
            //when
            List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");
    
            for (UsernameOnly usernameOnly : result) {
                System.out.println("usernameOnly = " + usernameOnly.getUsername());
            }
            //then
        }

     

     

    엔티티 정보를 모두 가져오는 open projection을 하고 username과 age를 둘다 변수에 넣어줍니다.

     

     

     

     

     

    클래스 기반의 projection

     

    실제 클래스가 있으니 프록시말고 실제 클래스를 넣습니다.

    package study.datajpa.repository;
    
    public class UsernameOnlyDto {
    
        private final String username;
    
        public UsernameOnlyDto(String username) {
            this.username = username;
        }
    
        public String getUsername() {
            return username;
        }
    }
    

     

     

    usernameOnlyDto를 찍어봤을때 프록시 클래스가 아닌 실제 클래스가 들어있습니다.

        @Test
        public void projectionsClass() throws Exception {
            //given
            Team teamA = new Team("teamA");
            em.persist(teamA);
    
            Member m1 = new Member("m1", 0, teamA);
            Member m2 = new Member("m2", 0, teamA);
            em.persist(m1);
            em.persist(m2);
    
            em.flush();
            em.clear();
    
    
            //when
            List<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("m1");
    
            for (UsernameOnlyDto usernameOnlyDto : result) {
                System.out.println("usernameOnlyDto = " + usernameOnlyDto);
                System.out.println("usernameOnlyDto.getUsername() = " + usernameOnlyDto.getUsername());
            }
    
            //then
        }

     

     

     

     

    동적으로 반환타입 지정도 가능합니다.

    <T> List<T> findProjectionsByUsername(@Param("username") String username, Class<T> type);

     

     

     

    중첩구조

     

    package study.datajpa.repository;
    
    public interface NestedClosedProjections {
    
        String getUsername();
        TeamInfo getTeam();
    
        interface TeamInfo {
            String getName();
        }
    
    }
    

     

     

    첫 엔티티는 close projection으로 잘 가져오지만, 두번째 엔티티 부터는 엔티티 전체를 가져옵니다.

     

     

     

    그래서 엔티티 하나 넘어가는 순간 쓰기가 좀 힘들어집니다.

     

     

    결론

    간단한 projection 정도는 쓸만하지만

    실무에서는 QueryDSL 쓰자.

     

     

     

     

     

     

    네이티브 쿼리

     

    (JPA를 사용할땐 실무에서는 되도록 쓰지 않는 것이 좋습니다.)

     

    스프링 데이터 JPA에서 제공하는 네이티브 쿼리 기능 입니다.

     

    제약 3가지 때문에 잘 이용하지 않습니다.

    1. Sort 파라미터를 통한 정렬이 정상 동작하지 않는 경우가 있습니다.
    2. 애플리케이션 로딩시점에 문법오류 확인이 불가합니다.
    3. 동적쿼리가 불가합니다.

     

    결론 : 네이티브 SQL을 DTO로 조회할때는 jdbcTemplate이나 myBatis를 쓰는 것이 좋습니다.

     

     

     

    최근에 나온 괜찮은 방법

    동적쿼리가 아닐때 쓰기 좋습니다.

     

    네이티브 쿼리 -> DTO로 가져올때 projection을 이용합니다.

     

    또한, pageable을 사용하는 것이 가능해 페이징과 정렬이 가능합니다.

     

     

    @Query를 이용해 네이티브 쿼리를 적고, nativeQuery = true를 해주시면 됩니다.

     

    Page를 사용하기 위해 필요한 countQuery도 따로 작성가능합니다.

     

     

     

    MemberRepository

        @Query(value = "select m.member_id as id, m.username, t.name as teamName " +
                "from member m left join team t",
                countQuery = "select count(*) from member",
                nativeQuery = true)
        Page<MemberProjection> findByNativeProjection(Pageable pageable);

     

        @Test
        public void nativeQuery() throws Exception {
            //given
            Team teamA = new Team("teamA");
            em.persist(teamA);
    
            Member m1 = new Member("m1", 0, teamA);
            Member m2 = new Member("m2", 0, teamA);
            em.persist(m1);
            em.persist(m2);
    
            em.flush();
            em.clear();
    
            //when
            Page<MemberProjection> result = memberRepository.findByNativeProjection(PageRequest.of(0, 10));
            List<MemberProjection> content = result.getContent();
    
            for (MemberProjection memberProjection : content) {
                System.out.println("memberProjection.getUsername() = " + memberProjection.getUsername());
                System.out.println("memberProjection.getTeamName() = " + memberProjection.getTeamName());
            }
    
            //then
        }