springframework/Spring Data JPA

Spring Data JPA : Entity Graph

Jungsoomin :) 2020. 11. 19. 00:11

Entity Graph 란.

  • Fetch 전략을 유연하게 설정할 수 있게 해준다.

현재 Entity 의 주인은 Comment , N 으로서 @ManyToOne , 기본 FETCH 전략은 EAGER

@Entity
public class Comment {

    @Id @GeneratedValue
    private Long id;

    private String title;

    private String comment;

    @ManyToOne
    private Post post;

    public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getComment() {
        return comment;
    }

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

    @Override
    public String toString() {
        return "Comment{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", comment='" + comment + '\'' +
                '}';
    }
}


@RunWith(SpringRunner.class)
@DataJpaTest
public class EntityGraphTest {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private CommentRepository commentRepository;

    @Test
    public void testEntityGraph(){
        Optional<Comment> selectedComment_opt = commentRepository.findById(1L);
    }
}



///
Hibernate: 
    select
        comment0_.id as id1_0_0_,
        comment0_.comment as comment2_0_0_,
        comment0_.post_id as post_id4_0_0_,
        comment0_.title as title3_0_0_,
        post1_.id as id1_1_1_,
        post1_.created as created2_1_1_,
        post1_.title as title3_1_1_ 
    from
        comment comment0_ 
    left outer join
        post post1_ 
            on comment0_.post_id=post1_.id 
    where
        comment0_.id=?
2020-11-18 23:17:20.078 TRACE 564 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]

@ManyToOne 의 FETCT 전략을 LAZY 로 변경하면 필요할 때에만 Join 으로 데이터를 가져오게 된다.

@Entity
public class Comment {

    @Id @GeneratedValue
    private Long id;

    private String title;

    private String comment;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getComment() {
        return comment;
    }

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

    @Override
    public String toString() {
        return "Comment{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", comment='" + comment + '\'' +
                ", post=" + post +
                '}';
    }
}

테스트 코드를 짜던 중에 Post Entity 가 Transient 상태이기 때문에 flush() 시키기 전에 저장해 달라는 친절한 에러메세지가 나와서 행복해했다. 1차 캐싱으로 인해 select 문은 날리지 않았다.

@RunWith(SpringRunner.class)
@DataJpaTest
public class EntityGraphTest {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private CommentRepository commentRepository;

    @Test
    public void testEntityGraph(){
        Post post = new Post();
        post.setTitle("Spring");

        Comment comment = new Comment();
        comment.setTitle("JPA");
        Post savedPost = postRepository.save(post);
        comment.setPost(savedPost);
        commentRepository.save(comment); // 주인 Entity 에서 영속화 시켜야만 한다. 주의.
        commentRepository.flush();

        Optional<Comment> selectedComment_opt = commentRepository.findById(2L);
        String commentToString = selectedComment_opt.get().toString();// toString 으로 인해 post 멤버변수도 찍혀야하는 상황
        System.out.println(commentToString);
    }
}

///

Hibernate: 
    insert 
    into
        post
        (created, title, id) 
    values
        (?, ?, ?)
2020-11-18 23:31:50.442 TRACE 10292 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [TIMESTAMP] - [null]
2020-11-18 23:31:50.443 TRACE 10292 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Spring]
2020-11-18 23:31:50.446 TRACE 10292 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [1]
Hibernate: 
    insert 
    into
        comment
        (comment, post_id, title, id) 
    values
        (?, ?, ?, ?)
2020-11-18 23:31:50.450 TRACE 10292 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [null]
2020-11-18 23:31:50.451 TRACE 10292 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2020-11-18 23:31:50.452 TRACE 10292 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [JPA]
2020-11-18 23:31:50.453 TRACE 10292 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [2]
Comment{id=2, title='JPA', comment='null', post=Post{id=1, title='Spring', created=null}}

EntityGraph 존재 이유

  • 설정 상태에서 필요한 경우 FETCH 전략을 바꾸고자한다.
  • @NamedEntityGraph 어노테이션 사용
  • name 속성에 EntityGraph 의 이름 지정
  • attributeNodes 속성에 @NamedAttributeNode로 연관관계 이름을 주면 된다.
@NamedEntityGraph(name = "Comment.post",attributeNodes = @NamedAttributeNode("post"))
@Entity
public class Comment {

    @Id @GeneratedValue
    private Long id;

    private String title;

    private String comment;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    public Post getPost() {
        return post;
    }

    public void setPost(Post post) {
        this.post = post;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getComment() {
        return comment;
    }

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

    @Override
    public String toString() {
        return "Comment{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", comment='" + comment + '\'' +
                ", post=" + post +
                '}';
    }
}

설정이 완료되면 사용은 Repository 인터페이스에서 한다.

  • 메서드에 @EntityGraph 어노테이션 사용
  • value 속성에 EntityGraph 이름 기술
  • type 속성에 EntityGraphType 기술
  • EntityGraphType 의 기본 값은 FETCH , 설정한 어트리뷰트는 EAGER , 나머지 어트리뷰트는 LAZY 로 가져오는 전략이다. 원시타입 어트리뷰트는 EAGER 로 가져온다. 
  • EntityGraphType.LOAD 는 설정한 어트리뷰트는 EAGER 로 가져오며 나머지 어트리뷰트는 기본 전략을 따른다.
public interface CommentRepository extends JpaRepository<Comment,Long> {

    @EntityGraph(value = "Comment.post",type = EntityGraph.EntityGraphType.LOAD)
    Optional<Comment> getById(Long id);
}

해당 메서드 호출시 LAZY 전략임에도 불구하고 @EntityGraph 에 의해 EAGER 전략으로 가져온다.

@Test
    public void loadCommentById(){
        Post post = new Post();
        post.setTitle("Spring");

        Comment comment = new Comment();
        comment.setTitle("JPA");
        Post savedPost = postRepository.save(post);
        comment.setPost(savedPost);
        commentRepository.save(comment); 
        commentRepository.flush();

        Optional<Comment> selectedComment_opt = commentRepository.getById(2L);
    }
    
 ///
 Hibernate: 
    select
        comment0_.id as id1_0_0_,
        post1_.id as id1_1_1_,
        comment0_.comment as comment2_0_0_,
        comment0_.post_id as post_id4_0_0_,
        comment0_.title as title3_0_0_,
        post1_.created as created2_1_1_,
        post1_.title as title3_1_1_ 
    from
        comment comment0_ 
    left outer join
        post post1_ 
            on comment0_.post_id=post1_.id 
    where
        comment0_.id=?
2020-11-18 23:50:17.293 TRACE 13864 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [2]
2020-11-18 23:50:17.307 TRACE 13864 --- [           main] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_0_0_] : [BIGINT]) - [2]
2020-11-18 23:50:17.307 TRACE 13864 --- [           main] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_1_1_] : [BIGINT]) - [1]

Entity 클래스에 @NameEntityGraph 없이 Repository 메서드@EntityGraph 어노테이션의 attributePaths 에 배열로 원하는 프로퍼티를 제시, FETCH 전략을 제공할 수 있다.

public interface CommentRepository extends JpaRepository<Comment,Long> {

    @EntityGraph(attributePaths = "post",type = EntityGraph.EntityGraphType.LOAD)
    Optional<Comment> getById(Long id);
}