当前位置: 首页 > 知识库问答 >
问题:

在Spring Boot上创建存储库的最佳实践是什么?

景令秋
2023-03-14

我想创建一对多的映射,就像Post有很多评论一样。我有两种添加评论的解决方案。第一种解决方案是为评论创建一个存储库,第二种解决方案是使用PostRepository并获取post并将评论添加到帖子中。每个解决方案都有自己的挑战。

在第一种解决方案中,为每个实体创建存储库会过多地增加存储库的数量,并且基于DDD,应该为Aggregate Roots创建存储库。

在第二个解决方案中,存在性能问题。若要加载、添加或删除嵌套实体,必须先加载根实体。若要添加实体,必须从用户存储库加载其他相关实体(如“注释实体”中的“用户实体”)。因此,这些额外的负载会导致速度和总性能下降。

加载、添加或移除嵌套实体的最佳实践是什么?

文件< code>Post.java

@Entity
@Table(name = "posts")
@Getter
@Setter
public class Post
{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Size(max = 250)
    private String description;

    @NotNull
    @Lob
    private String content;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Comment> comments = new HashSet<>();

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "user_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private User user;
}

文件注释.java

@Entity
@Table(name = "comments")
@Getter
@Setter
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Lob
    private String text;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "post_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "user_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private User user;
}
@Entity
@Table(name = "Users")
@Getter
@Setter
public class User
{   
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; 

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Comment> comments = new HashSet<>();

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private Set<Post> posts = new HashSet<>();
}

共有2个答案

戚正业
2023-03-14

N 1问题:在循环中获取数据,如果你有2000个帖子和评论数据,你需要避免获取每个数据。

// Ex: 2000 posts is fetched
for(Post post: userRepository.findById("1").getPosts()) {
   // fetching in loop: you go to database for each post(2000) and get comments of posts.
   Set<Comment> comments = post.getComments();
}

解决方案:使用自定义存储库为Post和fetch创建存储库。有很多方法可以急切地获取。例如:EntityGraph,FetchType。渴望,JPQL。。。

@Query(value = "select p from Post p fetch left join p.comments c where p.id=:postId)
public Set<Post> postsWithComments(@Param("postId") Long postId)

Set<Post> posts = postRepository.postWithComments(1L);

即使您在急切地获取数据时也需要小心,如果发布时有很多评论,只需使用另一个存储库进行评论即可。

public Set<Comment> findByPostId(String postId);
Set<Comment> comments = commentRepository.findByPostId(1L);

即使一篇文章有60000条评论。您需要使用分页进行获取,这在关键时刻非常有用。

public Page<Comment> findByPostId(Long postId, Pageable pageable);
Page<Comment> comments = commentRepository.findByPostId(1L, PageRequest.of(2000));
int loopCounter = comments.getTotalElements() % 2000 == 0 ? comments.getTotalElements() / 2000 : comments.getTotalElements() / 2000 + 1;
int i=1;
do{

   // do something
   i++;
}while(i <= loopCounter);

对于其他事项,您需要使用缓存策略来提高性能

您还需要定义什么是请求的响应时间,什么是实际响应时间。您可以将fetch与left join或简单的另一个请求一起使用。在长时间运行的流程中,您也可以使用异步操作。

高锦
2023-03-14

“最佳”没有很好的定义。但这里可能被认为是Spring数据团队在这个问题上的规范立场。

您绝对不应该每个实体都有一个存储库(s.您应该在JPA中每个表都有一个存储库吗?)。

原因当然不是你必须有很多类/接口。在实现时和运行时创建类和接口的成本非常低廉。有这么多人有点困难,这带来了一个重大问题。如果是这样,实体已经会引起问题。

原因是存储库处理聚合,而不是实体。尽管,不可否认的是,在基于JPA的代码中很难看出区别。所以你的问题归结为:什么应该是聚合。

至少部分答案已经在你的问题中:

在第二个解决方案中,存在性能问题。若要加载、添加或删除嵌套实体,必须先加载根实体。若要添加实体,必须从用户存储库加载其他相关实体(如“注释实体”中的“用户实体”)。因此,这些额外的负载会导致速度和总性能下降。

聚合和存储库的概念在微服务社区中被广泛采用,因为它们具有良好的可伸缩性。这当然与“速度和总体性能”不同,但肯定是相关的。

那么,如何将这两种观点结合起来呢?安德烈·B·潘菲洛夫(Andrey B.Panfilov)发表了一些评论:

@OneToMany实际上是@OneToFew,就像“一个人可以通过几个电话号码联系到”。但它只是描述了一种启发。

真正的规则是:集合应该对需要始终一致的类进行分组。典型的例子是带有行项目的采购订单。行项目本身没有意义。如果您修改行项目(或添加/删除一个),您可能必须更新采购订单,例如为了更新总价或为了保持最大值等约束。因此采购订单应该是包含其行项目的集合。

这也意味着您需要完全加载聚合。这反过来意味着它不能太大,因为否则您会遇到性能问题。

在您的PostCommentUser示例中,Post可能会与Comment形成聚合。但在大多数系统中,评论的数量接近无限并且可能很大。因此,我会投票支持将示例中的每个实体都作为自己的聚合。

有关聚合和存储库的更多信息,您可能会发现SpringDataJDBC、引用和聚合很有趣。这是关于Spring数据JDBC,而不是Spring数据JPA,但概念上的想法确实适用。

 类似资料:
  • 问题内容: 在Java桌面应用程序中存储密码的推荐方式是什么? 我希望用户只能输入一次凭证,而不会再次提示。 在个人项目中,我一直在使用Preferences API,但我假设这与以纯文本格式存储(安全方面)没有什么不同。 非常感谢 编辑: 非常感谢您的建议。毫无疑问,这似乎有些混乱,因为我可能还没有把问题弄清楚。 我将给出一个假设的情况: 假设我正在为远程数据库创建一个简单的前端,该前端使用用户

  • 问题内容: 处理和存储日期的最佳实践是什么,例如在企业Java应用程序中使用GregorianCalendar? 寻找反馈,我会将所有出色的答案合并为其他人可以使用的最佳实践。谢谢! 问题答案: 乔达就是要走的路。为什么呢 它具有比标准Date / Time API更强大,更直观的界面 日期/时间格式没有线程问题。java.text.SimpleDateFormat不是线程安全的(不是很多人都知道

  • 问题内容: 我的角度应用程序有2个控制器。我的问题是,当用户离开页面时,控制器不会保留数据。 如何将所选控制器上的选定数据存储到数据存储中,以便可以在其他控制器之间使用? 问题答案: 选项1-自定义 您可以利用专用的角度服务在控制器之间存储和共享数据(服务是单实例对象) 服务定义 在多个控制器中的用法 选项2-HTML5 您可以使用内置的浏览器本地存储并从任何地方存储数据 写作 读 看看这个很棒的

  • 问题内容: 我正在接管以前的开发人员的一些应用程序。当我通过Eclipse运行应用程序时,我看到内存使用率和堆大小增加了很多。经过进一步调查,我发现他们正在循环创建一个对象以及其他东西。 我开始经历并做一些清理。但是,我经历的时间越长,我就会遇到更多的问题,例如“这实际上会做什么?” 例如,他们没有在上述循环之外声明变量,而只是在循环中设置其值…而是在循环中创建了对象。我的意思是: 与 我不正确地

  • 问题内容: 我刚开始学习Go,并通读现有代码以学习“其他人的做法”。在这种情况下,遍历使用go“工作区”,尤其是与项目依赖关系有关的地方。 在处理各种Go项目时,使用一个或多个Go工作区(即$ GOPATH的定义)的常见(或存在)最佳实践是什么?我应该期望有一个类似于我所有项目的中央代码存储库的Go工作区,还是在我处理这些项目时都明确将其分解并设置$ GOPATH(有点像python) virtu

  • 本文向大家介绍ThreadPoolExecutor 创建方法最佳实践?相关面试题,主要包含被问及ThreadPoolExecutor 创建方法最佳实践?时的应答技巧和注意事项,需要的朋友参考一下 在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。 为什么呢? 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开