N+1问题:N+1问题是指在使用关系型数据库时,在获取一组对象及其关联对象时,产生额外的数据库查询的问题。其中N表示要获取的主对象的数量,而在获取每个主对象的关联对象时,会产生额外的1次查询。
N+1问题是很多项目中的通病。遗憾的是,直到数据量变得庞大时,我们才注意到它。不幸的是,当处理 N + 1 问题成为一项难以承受的任务时,代码可能会达到了一定规模。
【资料图】
在这篇文章中,我们将开始关注以下几点问题:
如何自动跟踪N+1问题?如何编写测试来检查查询计数是否超过预期值?N + 1 问题的一个例子假设我们正在开发管理动物园的应用程序。在这种情况下,有两个核心实体:Zoo和Animal。请看下面的代码片段:
@Entity@Table(name = "zoo")public class Zoo { @Id @GeneratedValue(strategy = IDENTITY) private Long id; private String name; @OneToMany(mappedBy = "zoo", cascade = PERSIST) private List animals = new ArrayList<>();}@Entity@Table(name = "animal")public class Animal { @Id @GeneratedValue(strategy = IDENTITY) private Long id; @ManyToOne(fetch = LAZY) @JoinColumn(name = "zoo_id") private Zoo zoo; private String name;}
现在我们想要检索所有现有的动物园及其动物。看看ZooService下面的代码。
@Service@RequiredArgsConstructorpublic class ZooService { private final ZooRepository zooRepository; @Transactional(readOnly = true) public List findAllZoos() { final var zoos = zooRepository.findAll(); return zoos.stream() .map(ZooResponse::new) .toList(); }}
此外,我们要检查一切是否顺利进行。简单的集成测试:
@DataJpaTest@AutoConfigureTestDatabase(replace = NONE)@Transactional(propagation = NOT_SUPPORTED)@Testcontainers@Import(ZooService.classclass ZooServiceTest { @Container static final PostgreSQLContainer> POSTGRES = new PostgreSQLContainer<>("postgres:13"); @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", POSTGRES::getJdbcUrl); registry.add("spring.datasource.username", POSTGRES::getUsername); registry.add("spring.datasource.password", POSTGRES::getPassword); } @Autowired private ZooService zooService; @Autowired private ZooRepository zooRepository; @Test void shouldReturnAllZoos() { /* data initialization... */ zooRepository.saveAll(List.of(zoo1, zoo2)); final var allZoos = assertQueryCount( () -> zooService.findAllZoos(), ofSelects(1) ); /* assertions... */ assertThat( ... ); }}
测试成功通过。但是,如果记录 SQL 语句,会注意到以下几点:
-- selecting all zoosselect z1_0.id,z1_0.name from zoo z1_0-- selecting animals for the first zooselect a1_0.zoo_id,a1_0.id,a1_0.name from animal a1_0 where a1_0.zoo_id=?-- selecting animals for the second zooselect a1_0.zoo_id,a1_0.id,a1_0.name from animal a1_0 where a1_0.zoo_id=?
如所见,我们select对每个 present 都有一个单独的查询Zoo。查询总数等于所选动物园的数量+1。因此,这是N+1问题。
这可能会导致严重的性能损失。尤其是在大规模数据上。
自动跟踪 N+1 问题当然,我们可以自行运行测试、查看日志和计算查询次数,以确定可行的性能问题。无论如何,这效率很低。。
有一个非常高效的库,叫做datasource-proxy。它提供了一个方便的 API 来javax.sql.DataSource使用包含特定逻辑的代理来包装接口。例如,我们可以注册在查询执行之前和之后调用的回调。该库还包含开箱即用的解决方案来计算已执行的查询。我们将对其进行一些改动以满足我们的需要。
查询计数服务首先,将库添加到依赖项中:
implementation "net.ttddyy:datasource-proxy:1.8"
现在创建QueryCountService. 它是保存当前已执行查询计数并允许您清理它的单例。请看下面的代码片段。
@UtilityClasspublic class QueryCountService { static final SingleQueryCountHolder QUERY_COUNT_HOLDER = new SingleQueryCountHolder(); public static void clear() { final var map = QUERY_COUNT_HOLDER.getQueryCountMap(); map.putIfAbsent(keyName(map), new QueryCount()); } public static QueryCount get() { final var map = QUERY_COUNT_HOLDER.getQueryCountMap(); return ofNullable(map.get(keyName(map))).orElseThrow(); } private static String keyName(Map map) { if (map.size() == 1) { return map.entrySet() .stream() .findFirst() .orElseThrow() .getKey(); } throw new IllegalArgumentException("Query counts map should consists of one key: " + map); }}
在那种情况下,我们假设_DataSource_我们的应用程序中有一个。这就是_keyName_函数否则会抛出异常的原因。但是,代码不会因使用多个数据源而有太大差异。
将SingleQueryCountHolder所有QueryCount对象存储在常规ConcurrentHashMap.
相反,_ThreadQueryCountHolder_将值存储在_ThreadLocal_对象中。但是_SingleQueryCountHolder_对于我们的情况来说已经足够了。
API 提供了两种方法。该get方法返回当前执行的查询数量,同时clear将计数设置为零。
BeanPostProccessor 和 DataSource 代理现在我们需要注册QueryCountService以使其从 收集数据DataSource。在这种情况下,BeanPostProcessor 接口就派上用场了。请看下面的代码示例。
@TestComponentpublic class DatasourceProxyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof DataSource dataSource) { return ProxyDataSourceBuilder.create(dataSource) .countQuery(QUERY_COUNT_HOLDER) .build(); } return bean; }}
我用注释标记类_@TestComponent_并将其放入_src/test_目录,因为我不需要对测试范围之外的查询进行计数。
如您所见,这个想法很简单。如果一个 bean 是DataSource,则将其包裹起来ProxyDataSourceBuilder并将QUERY_COUNT_HOLDER值作为QueryCountStrategy.
最后,我们要断言特定方法的已执行查询量。看看下面的代码实现:
@UtilityClasspublic class QueryCountAssertions { @SneakyThrows public static T assertQueryCount(Supplier supplier, Expectation expectation) { QueryCountService.clear(); final var result = supplier.get(); final var queryCount = QueryCountService.get(); assertAll( () -> { if (expectation.selects >= 0) { assertEquals(expectation.selects, queryCount.getSelect(), "Unexpected selects count"); } }, () -> { if (expectation.inserts >= 0) { assertEquals(expectation.inserts, queryCount.getInsert(), "Unexpected inserts count"); } }, () -> { if (expectation.deletes >= 0) { assertEquals(expectation.deletes, queryCount.getDelete(), "Unexpected deletes count"); } }, () -> { if (expectation.updates >= 0) { assertEquals(expectation.updates, queryCount.getUpdate(), "Unexpected updates count"); } } ); return result; }}
该代码很简单:
将当前查询计数设置为零。执行提供的 lambda。将查询计数给定的Expectation对象。如果一切顺利,返回执行结果。此外,您还注意到了一个附加条件。如果提供的计数类型小于零,则跳过断言。不关心其他查询计数时,这很方便。
该类Expectation只是一个常规数据结构。看下面它的声明:
@With@AllArgsConstructor@NoArgsConstructorpublic static class Expectation { private int selects = -1; private int inserts = -1; private int deletes = -1; private int updates = -1; public static Expectation ofSelects(int selects) { return new Expectation().withSelects(selects); } public static Expectation ofInserts(int inserts) { return new Expectation().withInserts(inserts); } public static Expectation ofDeletes(int deletes) { return new Expectation().withDeletes(deletes); } public static Expectation ofUpdates(int updates) { return new Expectation().withUpdates(updates); }}
最后的例子让我们看看它是如何工作的。首先,我在之前的 N+1 问题案例中添加了查询断言。看下面的代码块:
final var allZoos = assertQueryCount( () -> zooService.findAllZoos(), ofSelects(1));
不要忘记_DatasourceProxyBeanPostProcessor_在测试中作为 Spring bean 导入。
如果我们重新运行测试,我们将得到下面的输出。
Multiple Failures (1 failure) org.opentest4j.AssertionFailedError: Unexpected selects count ==> expected: <1> but was: <3>Expected :1Actual :3
所以,确实有效。我们设法自动跟踪 N+1 问题。是时候用 替换常规选择了JOIN FETCH。请看下面的代码片段。
public interface ZooRepository extends JpaRepository { @Query("FROM Zoo z LEFT JOIN FETCH z.animals") List findAllWithAnimalsJoined();}@Service@RequiredArgsConstructorpublic class ZooService { private final ZooRepository zooRepository; @Transactional(readOnly = true) public List findAllZoos() { final var zoos = zooRepository.findAllWithAnimalsJoined(); return zoos.stream() .map(ZooResponse::new) .toList(); }}
让我们再次运行测试并查看结果:
这意味着正确地跟踪了 N + 1 个问题。此外,如果查询数量等于预期数量,则它会成功通过。
结论事实上,定期测试可以防止 N+1 问题。这是一个很好的机会,可以保护那些对性能至关重要的代码部分。
标签:
上一篇 : 崔永辉调研推进保障性住房工作 环球微动态
下一篇 : 最后一页
最新推荐
N+1问题:N+1问题是指在使用关系型数据库时,在获取一组对象及其关联对
今天上午,市委书记崔永辉前往湖里、翔安、集美,围绕保障性住房建设使
618一到本人就忍不住买了护肤品、化妆品、漂亮裙裙、凉鞋等本就不多的
中新社北京6月13日电新德里消息:据路透社报道,印度消费者事务、食品
味道串得越离谱,越是别有风味。“年轻人的第一口茅台,也有可能是咖啡
一女大学生怀疑同校男生尾拍后称认错人,杭州师范大学:我校发生一起学
客厅很适合摆放一些植物,不仅能净化空气,还能观赏和美化环境,其实很
逆回购降息率先落地!本月MLF利率和LPR有望跟随下调,政策加码窗口或开
绿地香港:前5月合约销售金额66 47亿元,绿地香港,合同销售面积
6月8日至11日,由四川省人民政府与工业和信息化部主办的2023世界动力电
湖北省梁子湖区发布暴雨橙色预警梁子湖区气象台2023年06月13日01时31分
WiFi延迟忽高忽低的原因有很多,下面将从不同的角度进行解释。首先,Wi
新华社北京6月13日电全球人权治理高端论坛将于6月14日至15日在北京举行
中国光伏企业高管慕尼黑下飞机被带走一事引发关注。多家媒体报道显示,
6月13日,记者从中国—东盟博览会秘书处获悉,今年以来,东博会秘书处
5月30日,福州市房地产中介行业协会打响了中介收费调整“第一枪”,出
近日,省林业局、省发展改革委、省财政厅、省农业农村厅联合印发《安徽
1、截止2019年,宁波共有5个汽车站,具体如下:宁波汽车南站宁波汽车南
嘉峪关黑山岩画展现古代羌族在河西走廊的生活图景,张掖榆木山岩画最早
6月12日,河南省光电产业链专题会议在郑州召开,认真落实省委省政府部
今年淘宝推出每日一猜的活动迎接618的到来,其中哪系列是NB夏日潮流必
“零点快线”成为深夜抵深旅客高效、便捷、经济的新选择。深圳特区报记
来源:触乐不管你对BioWare是爱是恨,优秀的故事总是能带来回报。在电
6月12日苹果期货板块较上一交易日上涨2 14%,安德利领涨。 从资金流向
本报讯记者安宁报道为贯彻落实中央经济工作会议、农村工作会议和全
导读1、由于主域名被屏蔽,99网现已启动。把之前的com换给我就可以打开
1、《月寒一梦》是一篇连载于晋江文学城网的言情小说。2、作者是我们没
6月12日(周一),金山云(KC US)股价继续上涨,截至发稿,该股涨超13%,
1、贵大哪些学院好贵州大学阳明学院,位于贵州省贵阳市,是贵州大学内
德约刚刚拿冠军肯定称得上是历史第1人了,纳达尔和费德勒现在大满贯数
1、就现在的技术而已,还是液晶电视软屏好一点。2、当然硬屏和软屏都有
1、公交线路:机场巴士2路。2、全程约42 4公里从石家庄正定国际机场步
今天,大学路小编为大家带来了江苏师范院校考编排名全国有哪些好的二本
6月10-11日,由北京城建设计发展集团主办、天津轨道交通集团有限公司承
大家好,小石来为大家解答以上问题。简述弗兰克赫兹实验中能量转化过程
大家好,小福来为大家解答以上的问题。爱的谎言普法栏目剧在线观看大结
1、早稻田是私立大学,学费比较贵,看你说本科还是硕士吧。2、同时早稻
大家好,小城来为大家解答以上问题。win8开始菜单不见了怎么恢复,win8
日前,有投资者在投资者互动平台提问:董秘您好!请问您与微软、AMD是
想必现在有很多小伙伴对于爸爸的好大儿是什么意思,是骂人的吗方面的知
音频解说1、近来,江西“高校食堂吃出疑似老鼠头”工作继续发酵。7日上
使用氢气的低碳冶金技术、浸没在液体里的服务器、家用级的储能产品……
在金融从业者看来,我国正面临的老龄化加速现状既意味着巨大的挑战,但
去年小鹏推出中大型SUV车型G9,这是一款被小鹏寄予厚望,品牌向上突破
1、去新加坡吧,新加坡不错呢,我看之前众信他们家有个4天的半自由行。
点击下方卡片,关注“CVer”公众号AI CV重磅干货,第一时间送达今天Amu
直播吧6月11日讯米兰新闻网的主编AntonioVitiello报道,米兰正在强势追
1、额昨晚才通宵麻刚网看问题--责任鸡打冲锋鸡规则原比摸幺鸡第冲(打)(
另外,有一张李美慧丈夫曾文豪抱着儿子满脸笑容的照片,也是引发了不
《车贞淑医生》中严正化扮演的女主在当了20多年的家庭主妇后,重返职场
青海西宁“抗疫”女警花:疫情不退 警察不退
展现“艺科融合”创作实践,清华美院本科生毕业作品展揭幕!|焦点资讯
【世界独家】雷电、暴雨、大风、冰雹!上海目前“一蓝三黄”预警高挂!全市启动防汛防台四级响应行动
环球快看:一个容积为2.5升的塑料壶用它装酒精最多能装多少千克(一个容积为2 5l的塑料瓶用它装水)
当前焦点!OpenAI首席执行官同韩总统会面:建议韩国增加系统芯片产能 放宽规制营造AI生态系统
天天通讯!咖啡店评弹返场、多彩手工体验!社区刮起最炫非遗风!
全球短讯!浙江一铁路旁工厂起火逼停列车 现场黑烟滚滚火光冲天!所幸无伤亡!
直击2023中国经济传媒大会丨中国传媒大学新闻学院教授沈浩:GPT可服务于财经内容的标签化、情感化、个性化
俄媒:俄国防部公布乌军装甲车辆被摧毁视频,引用二战时期经典语录|每日热门
当前快看:热威电热首发过会,进入新能源汽车供应商体系进展遭问询
曼城挖角拜仁后防全能神将,7球1助攻堪称带刀后卫,6大豪门都想买他
恭喜发财三大生肖6月11日-6月19日 财运特吉,深得财神关照,正财偏财齐齐发 焦点热文
今日关注:小屏旗舰即将回归 华硕Zenfone 10性能配置备受瞩目
【世界独家】国宝画重点丨上天入海,到处“出差”!这件宝贝可不得了
直击2023中国经济传媒大会丨中国经济体制改革会副会长樊纲:复苏正在进行中 我们需要耐心-世界今头条
焦点讯息:AI前哨 | Open AI联合创始人Sam AItman:目前开源不是最佳路径
Copyright © 2015-2022 中公律师网版权所有 备案号:沪ICP备2022005074号-18 联系邮箱:5855973@qq.com