Hibernate JPA中的N+1 选择问题以及如何避免它

让我们通过一些例子来谈谈休眠实体中臭名昭着的N + 1选择问题以及如何解决它。您可能还在某些地方听说过N + 1查询问题。

什么是 N+1 选择问题?

N+1 选择问题是性能反模式,其中应用程序对数据库进行 N+1 个小查询,而不是 1 个查询获取所有必需的数据。

让我们以博客文章及其评论为例。在这个系统中,我们有帖子评论实体。每条评论都引用了各自的帖子。这意味着从“发布”到“评论”实体的关系遵循一到多映射

如果您正在考虑评论以对一种关系发布为多个,那么您也是正确的。

这两个实体的表值如下所示。

insert into post (id,title,content,author) values(1, 'Some title 1','Some long content 1','John Doe' );
insert into post (id,title,content,author) values(2, 'Some title 2','Some long content 2','John Doe' );
insert into post (id,title,content,author) values(3, 'Some title 3','Some long content 3','John Doe' );
insert into post (id,title,content,author) values(4, 'Some title 4','Some long content 4','John Doe' );
insert into post (id,title,content,author) values(5, 'Some title 5','Some long content 5','John Doe' );

insert into comment (id, content, author, post_id) values (1, 'some comment on post1 ', 'John Doe', 1);
insert into comment (id, content, author, post_id) values (2, 'some comment on post1 ', 'John Doe', 1);
insert into comment (id, content, author, post_id) values (3, 'some comment on post2 ', 'John Doe', 2);
insert into comment (id, content, author, post_id) values (4, 'some comment on post3 ', 'John Doe', 3);
insert into comment (id, content, author, post_id) values (5, 'some comment on post4 ', 'John Doe', 4);
insert into comment (id, content, author, post_id) values (6, 'some comment on post4 ', 'John Doe', 4);
insert into comment (id, content, author, post_id) values (7, 'some comment on post4 ', 'John Doe', 4);
insert into comment (id, content, author, post_id) values (8, 'some comment on post5 ', 'John Doe', 5);

如您所见,单个帖子包含多个评论,每条评论都指向一个帖子。因此,让我们使用这些实体设置一个弹簧启动项目,并从数据库中查询它们。

public interface CommentRepository extends JpaRepository<Comment, Integer> {

    List<Comment> findAll();
}
如果您想了解有关jpa存储库的更多信息,请阅读 春季JPA简介

此外,我还编写了一个命令行运行程序,以便在应用程序启动时触发查询。

@SpringBootApplication
public class SpringBootNPlusOneApplication implements CommandLineRunner {
    public static final Logger logger = LoggerFactory.getLogger(SpringBootNPlusOneApplication.class);

    @Autowired
    CommentRepository commentRepository;


    public static void main(String[] args) {
        SpringApplication.run(SpringBootNPlusOneApplication.class, args);
    }

    @Transactional
    @Override
    public void run(String... args) throws Exception {
        logger.info("Finding all comment objects");
        List<Comment> comments = commentRepository.findAll();
        for (Comment comment : comments) {
            logger.info("Comment [{}] from Post [{}]",comment.getContent(), comment.getPost().getTitle());
        }

    }
}
通过所有这些设置,让我们 在Spring boot应用程序中启用SQL日志并运行我们的应用程序。

如您所见,底层休眠框架正在创建一个查询来加载所有注释。但它也会在单独的查询上加载每个帖子。

想象一下,你有2000条评论和500个帖子。因此,将有一个查询从数据库中获取2000条评论。但是,还将有500多个查询到数据库,用于获取这些评论引用的每个帖子。在现实世界中,这对数据库来说工作量太大了。此外,这将需要更多的网络往返,这反过来又会增加整体处理时间。这就是我们所说的N+1查询问题或N+1休眠选择问题。

如何解决N+1查询问题?

N+1 问题的原因是休眠的急切加载特性。默认情况下,休眠将始终加载@ManyToOne子级(获取类型.EAGER)。因此,如果尚未从数据库加载每个 post 对象,它将加载该对象。

让我们来看看休眠的N + 1选择问题的可能解决方案以及它们如何适合。

最初的想法是,您可以懒惰地加载孩子。因此,要执行此操作,请将注释设置为@ManyToOne(提取 = FetchType.LAZY)。在后台,延迟加载会为子对象创建代理对象。 当我们访问子对象时,休眠将触发查询并加载它们。话虽如此,这可能看起来是一个好主意,但延迟加载并不是完美的解决方案

这是由于延迟加载背后的底层逻辑。启用延迟加载后,休眠将为“Comment.post”字段创建代理。访问 post 字段时,休眠将使用数据库中的值填充代理。是的,对于每个 Post 对象,仍会发生查询。

我们也可以在日志中看到这种行为。

正如您在此处看到的,我们仍然看到对 POST 表的查询。但是,当我们循环访问注释对象以访问时,就会发生这种情况。comment.getPost().getTitle()

解决方案

那么我们如何解决N+1问题呢?为此,我们需要回到SQL的一些基础知识。当我们必须从两个单独的表加载数据时,我们可以使用联接。因此,我们可以编写如下所示的单个查询,而不是使用多个查询。

select *
from comment c
         inner join post p on c.post_id = p.id
同样,我们可以在春季数据JPA中加入获取结果。为此,您需要添加自定义 @Query注释。以下是更改后的存储库。
public interface CommentRepository extends JpaRepository<Comment, Integer> {

    @Query("select c from Comment c join fetch c.post")
    List<Comment> findAll();

}
正如您在这里看到的,我们使用  join fetch 关键字在同一查询中加载  post 对象。让我们再次运行我们的应用程序并检查结果。

消除 N+1 选择休眠问题

如屏幕截图所示,对数据库只有一个查询。它在同一查询中与 post 表进行内部联接。此外,在处理注释时,没有新的查询进入 DB。

如何找到N+1问题?

查找 N+1 问题的最简单方法是通过 SQL 日志。在春季启动中显示 SQL 日志并对其进行分析会更容易。因此,在测试代码时,请确保启用跟踪日志以查找此类 N+1 的出现次数。

查找 N+1 问题的系统方法是开始查看实体映射和存储库查询。每当数据库中有对象列表时,您可能希望检查业务逻辑是否访问了其子实体。如果是,那么您应该选择加入“加入抓取”。

为什么你应该避免N+1问题?

原因很简单。

  1. N+1 问题会创建更多对数据库的查询。这意味着数据库将过载。
  2. 对数据库的更多查询会影响数据库和应用程序服务器的性能。每个查询和处理响应都需要更多的 CPU 周期。
  3. 每个额外的查询都会增加所有处理时间。由于每个查询都需要发送和接收查询和结果,因此处理时间会成比例地增加。
  4. 更长的处理时间意味着来自连接池的更多开放连接。这会影响其他请求,因为它们必须等待更长时间才能获得连接。

总结

最后,我们了解了休眠中的N+1问题是什么,以及如何使用连接获取来解决它们。如果您想尝试这些示例,可以随时在我们的GitHub帐户中找到代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值