Skip to content

Why does not @EntityGraph annotation in JPA need to use "distinct" keyword or "Set" data structure? #2430

@Junhyunny

Description

@Junhyunny

I found out some interesting thing. When @OneToMany relationship in JPA, there should be N+1 issues. We need to use fetch join in JPQL or @EntityGraph annotation to avoid performance issue. But, we encounter other problem which is overlapping entities because of Cartesian product. fetch join becomes inner join and @EntityGraph becomes left outer join in SQL. So we have to using distinct in JPQL or Set data structure in Java.

Here is my question. When using fetch join, there is overlapping entities problem. However, when using @EntityGraph annotation, we can't see overlapping entities problem.

Let me show example. Here is my data set.

post.id post.content post.title
1 this is the first post. first post
reply.id reply.content reply.post_id
1 first-reply-1 1
2 first-reply-2 1
3 first-reply-3 1
4 first-reply-4 1
5 first-reply-5 1
6 first-reply-6 1
7 first-reply-7 1
8 first-reply-8 1
9 first-reply-9 1
10 first-reply-10 1

And when we query like this.

select *
from test.post inner join test.reply on test.post.id = test.reply.post_id;

We expect data like this.
But @EntityGraph annotation doesn't work like this.

post.id post.content post.title reply.id reply.content reply.post_id
1 this is the first post. first post 1 first-reply-1 1
1 this is the first post. first post 2 first-reply-2 1
1 this is the first post. first post 3 first-reply-3 1
1 this is the first post. first post 4 first-reply-4 1
1 this is the first post. first post 5 first-reply-5 1
1 this is the first post. first post 6 first-reply-6 1
1 this is the first post. first post 7 first-reply-7 1
1 this is the first post. first post 8 first-reply-8 1
1 this is the first post. first post 9 first-reply-9 1
1 this is the first post. first post 10 first-reply-10 1

Test Code

Post Entity
package blog.in.action.post;

import blog.in.action.reply.Reply;
import lombok.*;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column
    private String title;

    @Column
    private String content;

    @OneToMany(mappedBy = "post")
    private List<Reply> replies;

    public void addReply(Reply reply) {
        if (replies == null) {
            replies = new ArrayList<>();
        }
        replies.add(reply);
    }
}
Reply Entity
package blog.in.action.reply;

import blog.in.action.post.Post;
import lombok.*;

import javax.persistence.*;

@Builder
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Reply {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column
    private String content;

    @ManyToOne
    @JoinColumn(name = "post_id")
    private Post post;
}
PostRepository repository
package blog.in.action.post;

import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;
import java.util.Set;

public interface PostRepository extends JpaRepository<Post, Long> {

    @Query(value = "SELECT p FROM Post p JOIN FETCH p.replies WHERE p.title = :title")
    List<Post> findByTitleFetchJoinWithoutDistinct(String title);

    @EntityGraph(attributePaths = {"replies"})
    @Query(value = "SELECT p FROM Post p WHERE p.title = :title")
    List<Post> findByTitleEntityGraphWithoutDistinct(String title);
}
PostRepositoryTest tests
package blog.in.action.post;

import blog.in.action.reply.Reply;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import javax.persistence.EntityManager;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@DataJpaTest
public class PostRepositoryTest {

    @Autowired
    private EntityManager em;

    @Autowired
    private PostRepository postRepository;

    Post getPost(String title, String content) {
        return Post.builder()
                .title(title)
                .content(content)
                .build();
    }

    void insertReply(Post post, String content) {
        for (int index = 0; index < 10; index++) {
            Reply reply = Reply.builder()
                    .content(content + index)
                    .post(post)
                    .build();
            post.addReply(reply);
            em.persist(reply);
        }
    }

    @BeforeEach
    public void setup() {

        Post post = getPost("first post", "this is the first post.");
        Post secondPost = getPost("second post", "this is the second post.");

        postRepository.save(post);
        postRepository.save(secondPost);

        insertReply(post, "first-reply-");
        insertReply(secondPost, "second-reply-");

        em.flush();
        em.clear();
    }

    @Test
    public void whenFindByTitleFetchJoinWithoutDistinct_thenJustOneQuery() {

        List<Post> posts = postRepository.findByTitleFetchJoinWithoutDistinct("first post");

        assertThat(posts.size()).isEqualTo(10);
    }

    @Test
    public void whenFindByTitleEntityGraphWithoutDistinct_thenJustOneQuery() {

        List<Post> posts = postRepository.findByTitleEntityGraphWithoutDistinct("first post");

        assertThat(posts.size()).isEqualTo(1);
    }
}
whenFindByTitleFetchJoinWithoutDistinct_thenJustOneQuery test
  • log
select post0_.id         as id1_0_0_,
       replies1_.id      as id1_1_1_,
       post0_.content    as content2_0_0_,
       post0_.title      as title3_0_0_,
       replies1_.content as content2_1_1_,
       replies1_.post_id as post_id3_1_1_,
       replies1_.post_id as post_id3_1_0__,
       replies1_.id      as id1_1_0__
from post post0_
         inner join reply replies1_ on post0_.id = replies1_.post_id
where post0_.title = ?
  • debug result
    image
whenFindByTitleEntityGraphWithoutDistinct_thenJustOneQuery test
  • log
select post0_.id         as id1_0_0_,
       replies1_.id      as id1_1_1_,
       post0_.content    as content2_0_0_,
       post0_.title      as title3_0_0_,
       replies1_.content as content2_1_1_,
       replies1_.post_id as post_id3_1_1_,
       replies1_.post_id as post_id3_1_0__,
       replies1_.id      as id1_1_0__
from post post0_
         left outer join reply replies1_ on post0_.id = replies1_.post_id
where post0_.title = ?
  • debug result
    image

Why does this phenomenon occur?

Full test code link

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: invalidAn issue that we don't feel is valid

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions