自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java 中 N+1 問題的集成測試

開發(fā) 前端
事實(shí)上,定期測試可以防止 N+1 問題。這是一個很好的機(jī)會,可以保護(hù)那些對性能至關(guān)重要的代碼部分。

N+1問題:N+1問題是指在使用關(guān)系型數(shù)據(jù)庫時,在獲取一組對象及其關(guān)聯(lián)對象時,產(chǎn)生額外的數(shù)據(jù)庫查詢的問題。其中N表示要獲取的主對象的數(shù)量,而在獲取每個主對象的關(guān)聯(lián)對象時,會產(chǎn)生額外的1次查詢。

N+1問題是很多項(xiàng)目中的通病。遺憾的是,直到數(shù)據(jù)量變得龐大時,我們才注意到它。不幸的是,當(dāng)處理 N + 1 問題成為一項(xiàng)難以承受的任務(wù)時,代碼可能會達(dá)到了一定規(guī)模。

在這篇文章中,我們將開始關(guān)注以下幾點(diǎn)問題:

  1. 如何自動跟蹤N+1問題?
  2. 如何編寫測試來檢查查詢計數(shù)是否超過預(yù)期值?

N + 1 問題的一個例子

假設(shè)我們正在開發(fā)管理動物園的應(yīng)用程序。在這種情況下,有兩個核心實(shí)體: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<Animal> 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;
}

現(xiàn)在我們想要檢索所有現(xiàn)有的動物園及其動物。看看ZooService下面的代碼。

@Service
@RequiredArgsConstructor
public class ZooService {
    private final ZooRepository zooRepository;

    @Transactional(readOnly = true)
    public List<ZooResponse> findAllZoos() {
        final var zoos = zooRepository.findAll();
        return zoos.stream()
                   .map(ZooResponse::new)
                   .toList();
    }
}

此外,我們要檢查一切是否順利進(jìn)行。簡單的集成測試:

@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
@Transactional(propagation = NOT_SUPPORTED)
@Testcontainers
@Import(ZooService.class
class 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 語句,會注意到以下幾點(diǎn):

-- selecting all zoos
select z1_0.id,z1_0.name from zoo z1_0
-- selecting animals for the first zoo
select a1_0.zoo_id,a1_0.id,a1_0.name from animal a1_0 where a1_0.zoo_id=?
-- selecting animals for the second zoo
select a1_0.zoo_id,a1_0.id,a1_0.name from animal a1_0 where a1_0.zoo_id=?

如所見,我們select對每個 present 都有一個單獨(dú)的查詢Zoo。查詢總數(shù)等于所選動物園的數(shù)量+1。因此,這是N+1問題。

這可能會導(dǎo)致嚴(yán)重的性能損失。尤其是在大規(guī)模數(shù)據(jù)上。

自動跟蹤 N+1 問題

當(dāng)然,我們可以自行運(yùn)行測試、查看日志和計算查詢次數(shù),以確定可行的性能問題。無論如何,這效率很低。。

有一個非常高效的庫,叫做datasource-proxy。它提供了一個方便的 API 來javax.sql.DataSource使用包含特定邏輯的代理來包裝接口。例如,我們可以注冊在查詢執(zhí)行之前和之后調(diào)用的回調(diào)。該庫還包含開箱即用的解決方案來計算已執(zhí)行的查詢。我們將對其進(jìn)行一些改動以滿足我們的需要。

查詢計數(shù)服務(wù)

首先,將庫添加到依賴項(xiàng)中:

implementation "net.ttddyy:datasource-proxy:1.8"

現(xiàn)在創(chuàng)建QueryCountService. 它是保存當(dāng)前已執(zhí)行查詢計數(shù)并允許您清理它的單例。請看下面的代碼片段。

@UtilityClass
public 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<String, QueryCount> map) {
        if (map.size() == 1) {
            return map.entrySet()
                       .stream()
                       .findFirst()
                       .orElseThrow()
                       .getKey();
        }
        throw new IllegalArgumentException("Query counts map should consists of one key: " + map);
    }
}

在那種情況下,我們假設(shè)_DataSource_我們的應(yīng)用程序中有一個。這就是_keyName_函數(shù)否則會拋出異常的原因。但是,代碼不會因使用多個數(shù)據(jù)源而有太大差異。

將SingleQueryCountHolder所有QueryCount對象存儲在常規(guī)ConcurrentHashMap.

相反,_ThreadQueryCountHolder_將值存儲在_ThreadLocal_對象中。但是_SingleQueryCountHolder_對于我們的情況來說已經(jīng)足夠了。

API 提供了兩種方法。該get方法返回當(dāng)前執(zhí)行的查詢數(shù)量,同時clear將計數(shù)設(shè)置為零。

BeanPostProccessor 和 DataSource 代理

現(xiàn)在我們需要注冊QueryCountService以使其從 收集數(shù)據(jù)DataSource。在這種情況下,BeanPostProcessor 接口就派上用場了。請看下面的代碼示例。

@TestComponent
public 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;
    }
}

我用注釋標(biāo)記類_@TestComponent_并將其放入_src/test_目錄,因?yàn)槲也恍枰獙y試范圍之外的查詢進(jìn)行計數(shù)。

如您所見,這個想法很簡單。如果一個 bean 是DataSource,則將其包裹起來ProxyDataSourceBuilder并將QUERY_COUNT_HOLDER值作為QueryCountStrategy.

最后,我們要斷言特定方法的已執(zhí)行查詢量??纯聪旅娴拇a實(shí)現(xiàn):

@UtilityClass
public class QueryCountAssertions {
    @SneakyThrows
    public static <T> T assertQueryCount(Supplier<T> 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;
    }
}

該代碼很簡單:

  1. 將當(dāng)前查詢計數(shù)設(shè)置為零。
  2. 執(zhí)行提供的 lambda。
  3. 將查詢計數(shù)給定的Expectation對象。
  4. 如果一切順利,返回執(zhí)行結(jié)果。

此外,您還注意到了一個附加條件。如果提供的計數(shù)類型小于零,則跳過斷言。不關(guān)心其他查詢計數(shù)時,這很方便。

該類Expectation只是一個常規(guī)數(shù)據(jù)結(jié)構(gòu)??聪旅嫠穆暶鳎?/p>

@With
@AllArgsConstructor
@NoArgsConstructor
public 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 問題案例中添加了查詢斷言??聪旅娴拇a塊:

final var allZoos = assertQueryCount(
    () -> zooService.findAllZoos(),
    ofSelects(1)
);

不要忘記
_DatasourceProxyBeanPostProcessor_在測試中作為 Spring bean 導(dǎo)入。

如果我們重新運(yùn)行測試,我們將得到下面的輸出。

Multiple Failures (1 failure)
    org.opentest4j.AssertionFailedError: Unexpected selects count ==> expected: <1> but was: <3>
Expected :1
Actual   :3

所以,確實(shí)有效。我們設(shè)法自動跟蹤 N+1 問題。是時候用 替換常規(guī)選擇了JOIN FETCH。請看下面的代碼片段。

public interface ZooRepository extends JpaRepository<Zoo, Long> {
    @Query("FROM Zoo z LEFT JOIN FETCH z.animals")
    List<Zoo> findAllWithAnimalsJoined();
}

@Service
@RequiredArgsConstructor
public class ZooService {
    private final ZooRepository zooRepository;

    @Transactional(readOnly = true)
    public List<ZooResponse> findAllZoos() {
        final var zoos = zooRepository.findAllWithAnimalsJoined();
        return zoos.stream()
                   .map(ZooResponse::new)
                   .toList();
    }
}

讓我們再次運(yùn)行測試并查看結(jié)果:

這意味著正確地跟蹤了 N + 1 個問題。此外,如果查詢數(shù)量等于預(yù)期數(shù)量,則它會成功通過。

結(jié)論

事實(shí)上,定期測試可以防止 N+1 問題。這是一個很好的機(jī)會,可以保護(hù)那些對性能至關(guān)重要的代碼部分。

責(zé)任編輯:武曉燕 來源: 今日頭條
相關(guān)推薦

2010-09-06 09:03:17

SQLselect語句

2010-08-23 09:50:20

Ruby on Rai

2018-11-30 08:38:24

裁員互聯(lián)網(wǎng)電商

2019-05-24 08:36:33

MySQL查詢SQL

2009-02-26 08:42:31

聯(lián)想裁員賠償標(biāo)準(zhǔn)

2020-11-16 07:26:25

賠償碼農(nóng)

2021-02-02 13:35:48

React插件N+1

2021-07-16 08:29:41

項(xiàng)目React必備插件

2009-03-09 09:19:00

802.11n集成

2021-04-06 11:50:30

SQL數(shù)據(jù)統(tǒng)計

2021-11-03 16:00:40

SQL流量序號

2022-12-25 16:38:14

后端KuberneteIter8

2022-03-18 09:42:54

JavaString

2009-03-17 09:56:00

802.11n測試無線網(wǎng)絡(luò)

2022-02-09 07:29:53

打印排列解法循環(huán)解法

2023-05-27 08:29:40

中臺阿里巴巴架構(gòu)

2021-08-10 07:57:03

算法鏈表倒數(shù)

2020-07-07 07:33:12

Java單元集成

2021-04-01 15:42:45

人工智能軟件測試

2017-03-30 08:23:50

測試前端代碼
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號