我发现了一些有趣的事情。当在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 = ?
- 调试结果
有人知道这件事吗?
2条答案
按热度按时间evrscar21#
对于实体图形,默认情况下应用不同的过滤。过滤由框架使用标识集在Java端执行。
它在5.2.10版本HHH-11569中实现
请参阅源代码
在HQL或JPQL查询的情况下,您可以使用PASS_DISTINCT_THROUGH提示来达到相同的效果。Simon提到的The best way to use the JPQL DISTINCT keyword with JPA and Hibernate中描述了详细信息。
yjghlzjz2#
EntityGraph和JOIN FETCH不相关,而且实现方式也不相同。
笛卡尔积的原因是从Reply到post的反向引用。如果是单向关系,就不会有笛卡尔积。
请阅读Vlad Mihalcea关于JOIN FETCH的优秀文章
https://vladmihalcea.com/jpql-distinct-jpa-hibernate/