让我们通过一些例子来谈谈休眠实体中臭名昭着的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();
}
此外,我还编写了一个命令行运行程序,以便在应用程序启动时触发查询。
@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());
}
}
}
如您所见,底层休眠框架正在创建一个查询来加载所有注释。但它也会在单独的查询上加载每个帖子。
想象一下,你有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
public interface CommentRepository extends JpaRepository<Comment, Integer> {
@Query("select c from Comment c join fetch c.post")
List<Comment> findAll();
}
消除 N+1 选择休眠问题
如屏幕截图所示,对数据库只有一个查询。它在同一查询中与 post 表进行内部联接。此外,在处理注释时,没有新的查询进入 DB。
如何找到N+1问题?
查找 N+1 问题的最简单方法是通过 SQL 日志。在春季启动中显示 SQL 日志并对其进行分析会更容易。因此,在测试代码时,请确保启用跟踪日志以查找此类 N+1 的出现次数。
查找 N+1 问题的系统方法是开始查看实体映射和存储库查询。每当数据库中有对象列表时,您可能希望检查业务逻辑是否访问了其子实体。如果是,那么您应该选择加入“加入抓取”。
为什么你应该避免N+1问题?
原因很简单。
- N+1 问题会创建更多对数据库的查询。这意味着数据库将过载。
- 对数据库的更多查询会影响数据库和应用程序服务器的性能。每个查询和处理响应都需要更多的 CPU 周期。
- 每个额外的查询都会增加所有处理时间。由于每个查询都需要发送和接收查询和结果,因此处理时间会成比例地增加。
- 更长的处理时间意味着来自连接池的更多开放连接。这会影响其他请求,因为它们必须等待更长时间才能获得连接。
总结
最后,我们了解了休眠中的N+1问题是什么,以及如何使用连接获取来解决它们。如果您想尝试这些示例,可以随时在我们的GitHub帐户中找到代码。