springframework/Spring Data JPA

Spring Data JPA : Projection

Jungsoomin :) 2020. 11. 19. 16:36

Projection 이란

  • 엔티티의 속성 중 원하는 속성만 select 해오는 기능
  • Closed Projecton , Open Projection 으로 나뉜다.
  • Interface , Class 기반의 2가지 방법을 제공한다.

해당 코드는 Projection 미사용 시 모든 레코드를 조회 *  하고 있는 상황을 보여준다.

public interface CommentRepository extends JpaRepository<Comment,Long> {

    List<Comment> findByPost_Id(Long id);
}

@Test
    public void getComment(){
        commentRepository.findByPost_Id(1L);
    }

/// 모든 레코드를 다 가져옴
Hibernate: 
    select
        comment0_.id as id1_1_,
        comment0_.best as best2_1_,
        comment0_.comment as comment3_1_,
        comment0_.down as down4_1_,
        comment0_.post_id as post_id7_1_,
        comment0_.title as title5_1_,
        comment0_.up as up6_1_ 
    from
        comment comment0_ 
    left outer join
        post post1_ 
            on comment0_.post_id=post1_.id 
    where
        post1_.id=?
2020-11-19 14:48:05.131 TRACE 18272 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

Closed Projection : 원하는 레코드만을 가져오는 방법

  • 인터페이스 or 클래스getter 를 선언하여 원하는 컬럼을 정한다.
  • JpaRepository 상속 클래스 메서드리턴타입 제네릭Projection 인터페이스 를 선언한다.
// 프로젝션 인터페이스에 원하는 레코드의 Getter 선언
public interface CommentSummary {

    String getComment();

    int getUp();

    int getDown();

}

public interface CommentRepository extends JpaRepository<Comment,Long> {
	// 리턴타입 제네릭에 프로젝션 인터페이스 선언
    List<CommentSummary> findByPost_Id(Long id);
}

Hibernate: 
    select
        comment0_.comment as col_0_0_,
        comment0_.up as col_1_0_,
        comment0_.down as col_2_0_ 
    from
        comment comment0_ 
    left outer join
        post post1_ 
            on comment0_.post_id=post1_.id 
    where
        post1_.id=?
2020-11-19 14:49:31.979 TRACE 8152 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

Open Projection

  • @Value + SpEL 을 사용하여 원하는 프로퍼티를 조합한 결과를 만들어낸다.
  • 결과적으로 컬럼을 전부 추출하고 조합하는 형태를 가지고 있다.
  • 성능최적화는 이룰 수 없으나, 원하는 데이터를 조합할 수 있게 된다.
public interface CommentSummary {

    String getComment();

    int getUp();

    int getDown();
	
    //Open Projection , @Value 에 표현식을 지정하여 가져온 Entity 객체의 프로퍼티를 조합시키고 있음
    @Value("#{target.up + ' ' +target.down}")
    String getVotes();

}

@Test
    public void getComment() {
        Post post = new Post();
        post.setTitle("JPA");

        Post savedPost = postRepository.save(post);

        Comment comment  =  new Comment();
        comment.setTitle("Spring");
        comment.setPost(savedPost);
        comment.setUp(10);
        comment.setDown(1);

        commentRepository.save(comment);
        commentRepository.flush();
        postRepository.flush();

       List<CommentSummary> selectedComment = commentRepository.findByPost_Id(1L);

        selectedComment.forEach( c -> {
           System.out.println(c.getVotes());
        });
    }
    
/// 모든 컬럼을 다 select 하게 된다.
Hibernate: 
    select
        comment0_.id as id1_1_,
        comment0_.best as best2_1_,
        comment0_.comment as comment3_1_,
        comment0_.down as down4_1_,
        comment0_.post_id as post_id7_1_,
        comment0_.title as title5_1_,
        comment0_.up as up6_1_ 
    from
        comment comment0_ 
    left outer join
        post post1_ 
            on comment0_.post_id=post1_.id 
    where
        post1_.id=?
2020-11-19 16:03:47.999 TRACE 9416 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2020-11-19 16:03:48.003 TRACE 9416 --- [           main] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_] : [BIGINT]) - [2]
10 1

Closed Projection 의 원하는 데이터 추출과 Open Projection 의 데이터 조합을 같이 사용할 수는 없는가.

  • Java 8 부터 제공되는 인터페이스의 default 메서드를 사용한다.
  • 해당 메서드에 원하는 Getter 를 선언해서 데이터를 조합하는 방식이다.
// Closed Projection 메서드 를 default 메서드에서 호출하여 원하는 결과를 뽑아 냄.
public interface CommentSummary {

    String getComment();

    int getUp();

    int getDown();

    default String getVote(){
        return getUp()+ " "+getDown();
    }

}

// 리포지토리 메서드의 리턴타입 제네릭에 선언하여 CommentSummary 를 사용하도록 함
public interface CommentRepository extends JpaRepository<Comment,Long> {

    List<CommentSummary> findByPost_Id(Long id);
}


//테스트 코드, Closed Projection 으로 컬럼을 추출한 뒤 추출한 컬럼을 조합하는 방식
	@Test
    public void getVote() {
        Post post = new Post();
        post.setTitle("JPA");

        Post savedPost = postRepository.save(post);

        Comment comment = new Comment();
        comment.setTitle("Spring");
        comment.setPost(savedPost);
        comment.setUp(10);
        comment.setDown(1);

        commentRepository.save(comment);

        List<CommentSummary> selectedComment = commentRepository.findByPost_Id(1L);

        selectedComment.forEach(c -> {
            System.out.println(c.getVote());
        });
    }
    
 /// 원하는 컬럼만을 추출하고 추출한 컬럼을 조합하는 모습을 보여줌
 Hibernate: 
    select
        comment0_.comment as col_0_0_,
        comment0_.up as col_1_0_,
        comment0_.down as col_2_0_ 
    from
        comment comment0_ 
    left outer join
        post post1_ 
            on comment0_.post_id=post1_.id 
    where
        post1_.id=?
2020-11-19 16:08:47.801 TRACE 18424 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2020-11-19 16:08:47.808 TRACE 18424 --- [           main] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([col_0_0_] : [VARCHAR]) - [null]
2020-11-19 16:08:47.815 TRACE 18424 --- [           main] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([col_1_0_] : [INTEGER]) - [10]
2020-11-19 16:08:47.817 TRACE 18424 --- [           main] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([col_2_0_] : [INTEGER]) - [1]
10 1

하나의 메서드에 여러가지 Projection 을 받기 위해서제네릭 사용이 필요하다.

  • 리턴 타입은 메서드 오버로딩에 영향을 주지 못한다.
  • 그러므로 파라미터에 제네릭타입의 클래스를 던져주는 것이 좋다.
public interface CommentSummary {

    String getComment();

    int getUp();

    int getDown();

    default String getVote(){
        return getUp()+ " "+getDown();
    }

}

//Repository 인터페이스 에 리턴타입을 제네릭으로 선언한 후 파라미터에 Class 타입을 선언하도록 변경시킨 모습
public interface CommentRepository extends JpaRepository<Comment,Long> {

    <T> List<T> findByPost_Id(Long id,Class<T> type);
}

// 테스트 코드에는 메서드 리턴타입에 대한 제네릭에 클래스 를 정하는 파라미터가 추가됬을 뿐이다.
@Test
    public void getVoteGeneric() {
        Post post = new Post();
        post.setTitle("JPA");

        Post savedPost = postRepository.save(post);

        Comment comment = new Comment();
        comment.setTitle("Spring");
        comment.setComment("Hibernate");
        comment.setPost(savedPost);
        comment.setUp(10);
        comment.setDown(1);

        commentRepository.save(comment);

        List<CommentOnly> selectedComment = commentRepository.findByPost_Id(1L, CommentOnly.class);

        selectedComment.forEach(c -> {
            System.out.println(c.getComment());
        });
    }
    
Hibernate: 
    select
        comment0_.comment as col_0_0_ 
    from
        comment comment0_ 
    left outer join
        post post1_ 
            on comment0_.post_id=post1_.id 
    where
        post1_.id=?
2020-11-19 16:16:32.170 TRACE 12820 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2020-11-19 16:16:32.177 TRACE 12820 --- [           main] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([col_0_0_] : [VARCHAR]) - [Hibernate]
Hibernate

클래스로 Projection을 만들 수 있으나 코드가 장황해진다.

public class CommentSummaryClass {

    private String comment;

    private int up;

    private int down;

    public CommentSummaryClass(String comment, int up, int down) {
        this.comment = comment;
        this.up = up;
        this.down = down;
    }

    public String getVote(){
        return getUp() + " " + getDown();
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public int getUp() {
        return up;
    }

    public void setUp(int up) {
        this.up = up;
    }

    public int getDown() {
        return down;
    }

    public void setDown(int down) {
        this.down = down;
    }
}