天天日报丨Java 中 N+1 问题的集成测试

2023-06-13 19:22:48 来源: 今日头条

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 问题。这是一个很好的机会,可以保护那些对性能至关重要的代码部分。

标签:

天天日报丨Java 中 N+1 问题的集成测试

N+1问题:N+1问题是指在使用关系型数据库时,在获取一组对象及其关联对

06-13 19:22:48

excel工龄工资计算公式年月_excel工龄计算到月

1、为什么要写成2012 01 01,建议使用标准的日期格式,比如2012-01-01

06-13 18:47:34

环球即时看!深圳市福田区个人消费者购买新车 最高补贴15000元

6月13日,据深圳发布信息显示,自6月16日起,个人消费者在纳入福田区限

06-13 18:00:39

“城市生活消费季”落地杭州 超6000万元消费券快来领取下单小时达|全球头条

618大促一直是消费市场的重要晴雨表,今年618也再一次掀起了全民消费热

06-13 17:58:14

发文禁止婚外情和出轨 温州企业回应:不想再上热搜-头条

近日,“温州一企业发文禁止婚外情和出轨”“企业回应发文件禁止婚外情

06-13 17:17:25

园林股份: 公司将结合实际经营及业务发展等情况,积极探讨相关方案的可行性,具体请以公司披露的信息为准-热推荐

园林股份(605303)06月13日在投资者关系平台上答复了投资者关心的问题。

06-13 17:05:24

世界速读:央行:5月新增信贷1.36万亿 社融增量为1.56万亿 M2增长11.6%

【央行:5月新增信贷1 36万亿M2增长11 6%】5月末,广义货币(M2)余额282

06-13 16:25:17

布林肯借炒作”中国间谍活动”甩锅特朗普,外交部:老戏码 全球视讯

6月13日,外交部发言人汪文斌主持例行记者会。有记者提问,美国国务卿

06-13 15:48:19

汽车打吊瓶是清洗什么(汽车打吊瓶是洗什么)

想必现在有很多小伙伴对于汽车打吊瓶是洗什么方面的知识都比较想要了解

06-13 15:24:08

【新要闻】【新时代新征程新伟业】强强联盟 助力智慧健康城市建设迈入“快车道”

6月12日,乌鲁木齐经济技术开发区(头屯河区)与深圳达实旗云健康科技

06-13 14:46:08

清华大学无锡应用技术研究院智能产业创新中心签约落地滨湖

该中心将引进张亚勤院士团队,致力于突破人工智能核心技术,培养智能产

06-13 14:10:28

机战新世纪什么时候出 公测上线时间预告 环球快讯

导读:最近很多玩家都在关注机战新世纪这款手游,想知道具体的公测时间

06-13 13:17:11

moved是什么意思_moved

1、moved[mu:vd]adj 感动的adj Youwillneverfailtobem

06-13 12:46:53

港股异动|蔚来-SW(09866)涨超6% 宣布调整全系车价及新车主权益 换电正式成为收费服务

智通财经获悉,蔚来-SW(09866)午前涨超6%,截至发稿,涨6 1%,报67港元

06-13 12:14:48

【焦点热闻】第二十届(2023)蓝筹年会|探索新发展模式,房地产进入高质量发展时代

第二十届(2023)蓝筹年会|探索新发展模式,房地产进入高质量发展时代

06-13 11:43:26

狂风暴雨导致大树倒伏,南京鼓楼城管全力排险 天天看热讯

扬子晚报网6月11日讯(记者张可)6月10日晚,南京遭遇强对流天气。狂风

06-13 11:14:26

市公安机关“清风2023”专项行动成效显著 筑牢农村防赌治赌第一道防线

记者从市公安局获悉,2022年11月以来,天津公安机关全力开展打击农村赌

06-13 10:44:16

崩坏星穹铁道1.1罗浮仙舟新增书籍位置在哪

在崩坏星穹铁道游戏中,用户们会遇到崩坏星穹铁道1 1罗浮仙舟新增书籍

06-13 10:12:04

公益传奇吧_传奇私服吧

1、可以不过不能让网吧外的人玩因为外网需要开端口你是没有权限的。本

06-13 09:59:56

恒指低开0.25% 恒生科技指数低开0.02%_环球实时

证券时报e公司讯,恒指低开0 25%,恒生科技指数低开0 02%,蔚来涨超4%

06-13 09:21:55

新资讯:江苏国川建设有限公司_关于江苏国川建设有限公司简述

1、江苏国川建设有限公司于2006年08月16日成立。2、法定代表人殷勇,公

06-13 09:07:08

“模拟实战”提升基层党建工作质量

中国石化新闻网讯近日,共享东营分公司人力资源业务服务部开展了一

06-13 08:34:01

北约举行空军演习 日本成除瑞典外唯一非北约参加国|每日速读

【环球网报道】综合“德国之声”、日本《产经新闻》报道,当地时间12日

06-13 07:53:49

钱太紧,美初创企业面临“大规模灭绝事件”,可能迎来倒闭潮

对此,美国《华尔街日报》分析称,2021年市场上风投资金非常充裕,美国

06-13 06:57:27

全球热消息:伟岸蟑螂末日三部曲是哪三部_伟岸蟑螂

1、末日蟑螂是日本出版的一部科幻小说,它是2007年写的。2、蟑螂是个小

06-13 05:58:08

大队委员竞选演讲稿优秀篇三分钟_大队委员竞选演讲稿_世界观焦点

1、尊敬的老师、亲爱的同学:大家好!我是来五(三)班的呃呃呃。2、是

06-13 04:57:41

天天简讯:国家发改委已下达以工代赈中央投资73亿元

将吸纳20余万人在家门口务工就业(副题)中工网北京6月12日电(工人日

06-13 03:05:23

“救命药”短缺 美国全球找药

“救命药”短缺美国全球找药,顺铂,美国,药品,仿制药,注射液,fda,救命药

06-13 00:16:49

新款宝马M5渲染图曝光 外观更帅了 搭载XM同款混动

不久前,一辆覆盖了重重伪装的宝马新车亮相纽博格林赛道进行测试,

06-12 22:26:14

讯息:华创云信: 华创云信关于股东集中竞价减持股份时间届满的公告

华创云信:华创云信关于股东集中竞价减持股份时间届满的公告

06-12 21:32:18

MiniLED画质巅峰!海信电视E8获多平台大咖联袂推荐 环球短讯

每年一度的618作为家电产品销售旺季,也是电视升级换代的最佳时机。近

06-12 20:43:44

全球实时:“中央厨房”让学生吃上放心午餐

下锅、翻炒、加料、出锅……近日,在位于广安区临港经开区的力奥智能中

06-12 20:06:05

公司请假条的范本 天天新消息

公司请假条的范本(通用3篇)公司请假条的范本篇1尊敬的领导:您好我将

06-12 19:28:23

先导智能(300450.SZ)拟斥2亿元至3亿元回购公司股份_焦点要闻

智通财经讯,先导智能(300450 SZ)发布回购公司股份方案,本次回购股份

06-12 19:10:01

淮安市档案馆(关于淮安市档案馆介绍)|环球聚看点

来为大家讲解以上的问题。淮安市档案馆,安市档案馆介绍这个很多人还不

06-12 18:01:19

珠海地区大学_广州珠海大学相关内容简介介绍

广州珠海大学创办于1947年8月,是陈济棠为了罗致和培植人才、企图东山

06-12 17:53:01

6月12日晚盘:黄金td下午收盘450.81元 关注本周的通胀数据_焦点快播

周一(6月12日)下午,北京时间16:27,黄金td下午收盘价450 81元 克,涨幅0 28%。

06-12 16:48:28

俄乌双方再次交换在押人员

据央视新闻:当地时间11日,俄罗斯国防部发布公告称,当天有94名俄罗斯

06-12 15:50:01

6月12日东明石化油品报价上涨

产品6月11日6月12日涨跌单位:元 吨0 柴油71507130-20元 吨92 汽油8530

06-12 14:58:29

氧气中毒的症状及处理_氧气中毒_全球观速讯

1、“氧中毒”一般发生在长期吸氧的病人中。2、尽管适当吸氧能提高人体

06-12 14:04:45

【全球独家】古代船的别称_古代船

1、我国古代造船起步于遥远的原始社会的新石器时代,历史悠久,源远流

06-12 12:51:24

【世界速看料】警惕培训班退费骗局!

中消协提示警惕培训班退费骗局针对近期有不法分子以培训班退费名义实施

06-12 11:36:15

【环球报资讯】A股: 新股国科军工发行申购,发行价43.67元,股民中签会破发吗?

但在机构询价结束后,国科军工的发行总数为3667万股,对应43 67元的发

06-12 11:03:20

股价抢先异动 松发股份跨界信披迟来

松发股份(603268)在二级市场上的接连大涨让投资者赚得盆满钵满,公司

06-12 10:04:53

赏之味(08096)附属与寰信工程坊及好时工程公司就装修工程分别订立装修协议甲及装修协议乙-环球观点

赏之味(08096)发布公告,于2022年12月2日,公司间接全资附属公司富域分别

06-12 09:09:57

快播:新股消息 | 友芝友生物再度递表港交所 尚未从商业化候选药品中获得收入

智通财经APP获悉,据港交所6月11日披露,WuhanYZYBiopharmaCo ,LTD (武

06-12 07:40:31

俄驻美大使:美国无意通过外交途径解决俄乌冲突|天天观焦点

俄乌冲突升级以来,西方国家持续不断向乌克兰提供武器支持。6月9日,美

06-12 01:44:31

鸿均老祖的师傅是谁啊(鸿均) 头条

1、鸿均老祖是天地宇宙洪荒之祖(其实就是道教的开山鼻祖),也叫元阳

06-11 21:20:53

关于铁路计次票、定期票 如何购买使用

为进一步方便城市间人员乘车往来交流,满足广大旅客差异化出行需求,国

06-11 20:48:27

国家乡村振兴局开展农村厕所革命“提质年”-环球观天下

新华社北京6月9日电(记者侯雪静、郁琼源)国家乡村振兴局局长刘焕鑫9

06-11 20:39:29

excel工龄工资计算公式年月_excel工龄计算到月
环球即时看!深圳市福田区个人消费者购买新车 最高补贴15000元
“城市生活消费季”落地杭州 超6000万元消费券快来领取下单小时达|全球头条
发文禁止婚外情和出轨 温州企业回应:不想再上热搜-头条
园林股份: 公司将结合实际经营及业务发展等情况,积极探讨相关方案的可行性,具体请以公司披露的信息为准-热推荐
世界速读:央行:5月新增信贷1.36万亿 社融增量为1.56万亿 M2增长11.6%
布林肯借炒作”中国间谍活动”甩锅特朗普,外交部:老戏码 全球视讯
汽车打吊瓶是清洗什么(汽车打吊瓶是洗什么)
【新要闻】【新时代新征程新伟业】强强联盟 助力智慧健康城市建设迈入“快车道”
清华大学无锡应用技术研究院智能产业创新中心签约落地滨湖
机战新世纪什么时候出 公测上线时间预告 环球快讯
moved是什么意思_moved
港股异动|蔚来-SW(09866)涨超6% 宣布调整全系车价及新车主权益 换电正式成为收费服务
【焦点热闻】第二十届(2023)蓝筹年会|探索新发展模式,房地产进入高质量发展时代
狂风暴雨导致大树倒伏,南京鼓楼城管全力排险 天天看热讯
市公安机关“清风2023”专项行动成效显著 筑牢农村防赌治赌第一道防线
崩坏星穹铁道1.1罗浮仙舟新增书籍位置在哪
公益传奇吧_传奇私服吧
恒指低开0.25% 恒生科技指数低开0.02%_环球实时
新资讯:江苏国川建设有限公司_关于江苏国川建设有限公司简述
“模拟实战”提升基层党建工作质量
北约举行空军演习 日本成除瑞典外唯一非北约参加国|每日速读
钱太紧,美初创企业面临“大规模灭绝事件”,可能迎来倒闭潮
全球热消息:伟岸蟑螂末日三部曲是哪三部_伟岸蟑螂
大队委员竞选演讲稿优秀篇三分钟_大队委员竞选演讲稿_世界观焦点
天天简讯:国家发改委已下达以工代赈中央投资73亿元
“救命药”短缺 美国全球找药
新款宝马M5渲染图曝光 外观更帅了 搭载XM同款混动
讯息:华创云信: 华创云信关于股东集中竞价减持股份时间届满的公告
MiniLED画质巅峰!海信电视E8获多平台大咖联袂推荐 环球短讯
全球实时:“中央厨房”让学生吃上放心午餐
公司请假条的范本 天天新消息
先导智能(300450.SZ)拟斥2亿元至3亿元回购公司股份_焦点要闻
淮安市档案馆(关于淮安市档案馆介绍)|环球聚看点
珠海地区大学_广州珠海大学相关内容简介介绍
6月12日晚盘:黄金td下午收盘450.81元 关注本周的通胀数据_焦点快播
俄乌双方再次交换在押人员
6月12日东明石化油品报价上涨
氧气中毒的症状及处理_氧气中毒_全球观速讯
【全球独家】古代船的别称_古代船
【世界速看料】警惕培训班退费骗局!
【环球报资讯】A股: 新股国科军工发行申购,发行价43.67元,股民中签会破发吗?
股价抢先异动 松发股份跨界信披迟来
赏之味(08096)附属与寰信工程坊及好时工程公司就装修工程分别订立装修协议甲及装修协议乙-环球观点
快播:新股消息 | 友芝友生物再度递表港交所 尚未从商业化候选药品中获得收入
俄驻美大使:美国无意通过外交途径解决俄乌冲突|天天观焦点
鸿均老祖的师傅是谁啊(鸿均) 头条
关于铁路计次票、定期票 如何购买使用
国家乡村振兴局开展农村厕所革命“提质年”-环球观天下
北京高考成绩6月25日公布,27日启动本科志愿填报
X 广告
行业动态
X 广告

Copyright ©  2015-2022 亚太植物网版权所有  备案号:沪ICP备2020036824号-11   联系邮箱: 562 66 29@qq.com