spring-data-jpa 为什么JPA中的@EntityGraph注解不需要使用“distinct”关键字或“Set”数据结构?

ghhaqwfi  于 2022-11-10  发布在  Spring
关注(0)|答案(2)|浏览(156)

我发现了一些有趣的事情。当在JPA中使用@OneToMany关系时,应该会有N+1问题。我们需要在JPQL中使用fetch join@EntityGraph注解来避免性能问题。但是,我们还遇到了其他问题,即由于Cartesian product而导致实体重叠。在SQL中,fetch join变为inner join@EntityGraph变为left outer join。因此我们必须在JPQL中使用distinct或在Java中使用Set数据结构。
这里是我的问题。当使用fetch join时,存在重叠实体问题。但是,当使用@EntityGraph注解时,我们看不到重叠实体问题。
让我给你们看一个例子。这是我的数据集。
| post.id | post.content | post.title |
| - -|- -|- -|
| 一个|这是第一篇文章。|第一柱|
| reply.id | reply.content |reply.post:|
| - -|- -|- -|
| 一个|第一次答复-1|一个|
| 2个|第一次答复-2|一个|
| 三个|第一次答复-3|一个|
| 四个|第一次答复-4|一个|
| 五个|第一次答复-5|一个|
| 六个|第一次答复-6|一个|
| 七个|第一次答复-7|一个|
| 八个|第一次答复-8|一个|
| 九个|第一次答复-9|一个|
| 10个|第一次答复-10|一个|
而当我们这样查询的时候。

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

我们期待这样的数据,但是@EntityGraph注解并不是这样的。
| post.id | post.content | post.title | reply.id | reply.content |reply.post:|
| - -|- -|- -|- -|- -|- -|
| 一个|这是第一篇文章。|第一柱|一个|第一次答复-1|一个|
| 一个|这是第一篇文章。|第一柱|2个|第一次答复-2|一个|
| 一个|这是第一篇文章。|第一柱|三个|第一次答复-3|一个|
| 一个|这是第一篇文章。|第一柱|四个|第一次答复-4|一个|
| 一个|这是第一篇文章。|第一柱|五个|第一次答复-5|一个|
| 一个|这是第一篇文章。|第一柱|六个|第一次答复-6|一个|
| 一个|这是第一篇文章。|第一柱|七个|第一次答复-7|一个|
| 一个|这是第一篇文章。|第一柱|八个|第一次答复-8|一个|
| 一个|这是第一篇文章。|第一柱|九个|第一次答复-9|一个|
| 一个|这是第一篇文章。|第一柱|10个|第一次答复-10|一个|

测试代码
发布实体
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);
    }
}
回复实体
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;
}
后资料档案库资料档案库
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);
}
存储库测试后测试
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);
    }
}
when依标题寻找撷取链接但不含相异_then只有一个查询测试
  • 计程仪
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 = ?
  • 调试结果x1c 0d1x
whenFindByTitleEntityGraphWithoutDistinct_then仅一个查询测试
  • 计程仪
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 = ?
  • 调试结果

有人知道这件事吗?

完整测试代码链接
evrscar2

evrscar21#

对于实体图形,默认情况下应用不同的过滤。过滤由框架使用标识集在Java端执行。
它在5.2.10版本HHH-11569中实现
请参阅源代码

org/hibernate/hql/internal/ast/QueryTranslatorImpl.java

public List list(SharedSessionContractImplementor session, QueryParameters queryParameters)
            throws HibernateException {
...
        final boolean needsDistincting = (
                query.getSelectClause().isDistinct() ||
                getEntityGraphQueryHint() != null ||  //In case query has Entity Graph HINT applies distincting of result records
                hasLimit )
        && containsCollectionFetches();
...
        if ( needsDistincting ) {
            int includedCount = -1;
            // NOTE : firstRow is zero-based
            int first = !hasLimit || queryParameters.getRowSelection().getFirstRow() == null
                        ? 0
                        : queryParameters.getRowSelection().getFirstRow();
            int max = !hasLimit || queryParameters.getRowSelection().getMaxRows() == null
                        ? -1
                        : queryParameters.getRowSelection().getMaxRows();
            List tmp = new ArrayList();
            IdentitySet distinction = new IdentitySet();
            for ( final Object result : results ) {
                if ( !distinction.add( result ) ) {
                    continue;
                }
                includedCount++;
                if ( includedCount < first ) {
                    continue;
                }
                tmp.add( result );
                // NOTE : ( max - 1 ) because first is zero-based while max is not...
                if ( max >= 0 && ( includedCount - first ) >= ( max - 1 ) ) {
                    break;
                }
            }
            results = tmp;
        }

在HQL或JPQL查询的情况下,您可以使用PASS_DISTINCT_THROUGH提示来达到相同的效果。Simon提到的The best way to use the JPQL DISTINCT keyword with JPA and Hibernate中描述了详细信息。

yjghlzjz

yjghlzjz2#

EntityGraph和JOIN FETCH不相关,而且实现方式也不相同。
笛卡尔积的原因是从Reply到post的反向引用。如果是单向关系,就不会有笛卡尔积。
请阅读Vlad Mihalcea关于JOIN FETCH的优秀文章
https://vladmihalcea.com/jpql-distinct-jpa-hibernate/

相关问题