在本文中,我們學習瞭如何實現和測試CompositeItemReader,它允許我們按特定順序處理來自多個來源的資料。透過將讀取器連結在一起,我們可以按特定順序處理來自檔案、資料庫或其他來源的資料。
在 Spring Batch中,CompositeItemReader是一種將多個ItemReader例項組合成單個讀取器的工具。當我們需要從多個來源或按特定順序讀取資料時,這特別有用。例如,我們可能希望同時從資料庫和檔案中讀取記錄,或者按特定順序處理來自兩個不同表的資料。
CompositeItemReader簡化了批處理作業中多個讀取器的處理,確保高效靈活的資料處理。在本教程中,我們將介紹在 Spring Batch 中CompositeItemReader的實現,並檢視示例和測試用例以驗證其行為。
什麼是CompositeItemReader
CompositeItemReader的工作原理是將讀取過程委託給ItemReader例項列表。它按照定義順序從每個讀取器讀取專案,確保按順序處理資料。
這在以下場景中尤其有用:
- 從多個資料庫或表中讀取
- 合併檔案和資料庫中的資料
- 按特定順序處理來自不同來源的資料
此外,CompositeItemReader是org.springframework.batch.item.support包的一部分,它是在 Spring Batch 5.2.0 中引入的。實現CompositeItemReader
讓我們來看一個示例,其中我們從兩個不同的來源讀取資料:一個平面檔案和一個資料庫。目標是將來自兩個來源的產品資料合併為一個流以進行批處理。一些產品在平面檔案中,而其他產品在資料庫中,確保所有可用記錄一起處理。
1. 建立產品類別
在設定讀取器之前,我們需要一個Product類來表示正在處理的資料的結構。此類封裝了有關產品的詳細資訊,例如其 ID、名稱、庫存情況和價格。我們將在從 CSV 檔案和資料庫讀取時使用此模型,以確保資料處理的一致性。
Product類充當我們的讀者和批處理作業之間的資料傳輸物件 (DTO):
public class Product { private Long productId; private String productName; private Integer stock; private BigDecimal price; public Product(Long productId, String productName, Integer stock, BigDecimal price) { this.productId = productId; this.productName = productName; this.stock = stock; this.price = price; } <font>// Getters and Setters<i> }
|
Product類表示批處理作業將處理的每條記錄。現在我們的資料模型已準備就緒,我們將為CSV 檔案和資料庫建立單獨的ItemReader元件。2.產品資料的平面檔案讀取器
第一個讀取器使用FlatFileItemReader從 CSV 檔案獲取資料。我們將其配置為讀取分隔檔案 ( products.csv ) 並將其欄位對映到Product類:
@Bean public FlatFileItemReader<Product> fileReader() { return new FlatFileItemReaderBuilder<Product>() .name(<font>"fileReader") .resource(new ClassPathResource("products.csv")) .delimited() .names("productId", "productName", "stock", "price") .linesToSkip(1) .targetType(Product.class) .build(); }
|
這裡,delimited()方法確保資料欄位使用分隔符(預設情況下為逗號)分隔。names ()方法定義與Product類的屬性匹配的列名,而targetType(Product.class)方法將欄位對映到類屬性。3.產品資料資料庫讀取器
接下來,我們定義一個JdbcCursorItemReader來從名為products的資料庫表中檢索產品資料。此讀取器執行 SQL 查詢來獲取產品詳細資訊並將其對映到我們的Product類。
下面是資料庫讀取器的實現:
@Bean public JdbcCursorItemReader<Product> dbReader(DataSource dataSource) { return new JdbcCursorItemReaderBuilder<Product>() .name(<font>"dbReader") .dataSource(dataSource()) .sql("SELECT productid, productname, stock, price FROM products") .rowMapper((rs, rowNum) -> new Product( rs.getLong("productid"), rs.getString("productname"), rs.getInt("stock"), rs.getBigDecimal("price"))) .build(); }
|
JdbcCursorItemReader使用遊標從資料庫中一次一行地讀取產品記錄,從而提高批處理效率。rowMapper()函式將結果集中的每一列對映到Product類中的相應欄位。使用CompositeItemReader組合閱讀器
現在我們的 CSV 和資料庫讀取器都已配置為讀取產品資料,我們可以使用CompositeItemReader將它們整合:
@Bean public CompositeItemReader<Product> compositeReader() { return new CompositeItemReader<>(Arrays.asList(fileReader(), dbReader())); }
|
透過配置我們的CompositeItemReader,我們可以順序處理來自多個來源的資料。最初,FlatFileItemReader從 CSV 檔案中讀取產品記錄,將每行解析為Product物件。處理完檔案中的所有行後,JdbcCursorItemReader接管並開始直接從資料庫獲取產品資料。
配置批處理作業
一旦我們為 CSV 檔案和資料庫定義了讀取器,下一步就是配置批處理作業本身。在 Spring Batch 中,一個作業由多個步驟組成,其中每個步驟處理處理管道的特定部分:
@Bean public Job productJob(JobRepository jobRepository, Step step) { return new JobBuilder(<font>"productJob", jobRepository) .start(step) .build(); } @Bean public Step step(ItemReader compositeReader, ItemWriter productWriter) { return new StepBuilder("productStep", jobRepository) .<Product, Product>chunk(10, transactionManager) .reader(compositeReader) .writer(productWriter) .build(); }
|
在這種情況下,我們的工作包含一個步驟,即讀取產品資料、以10 個塊的形式處理它,並將其寫入所需的輸出。productJob bean 負責定義批處理作業。它從配置為處理產品資料處理的productStep開始執行。
透過此設定,我們的批處理作業首先使用CompositeItemReader從兩個來源讀取產品資料,以 10 個資料塊為單位進行處理,然後使用productWriter()寫入轉換或過濾後的資料。這確保了批處理管道順暢而高效。
執行批處理作業
現在我們已經配置了讀取器和作業,下一步是執行批處理作業並觀察CompositeItemReader的行為。我們將在 Spring Boot 應用程式中執行該作業,以檢視它如何處理來自 CSV 檔案和資料庫的資料。
為了以程式設計方式觸發批處理作業,我們需要使用JobLauncher。這使我們能夠啟動作業並監視其進度:
@Bean public CommandLineRunner runJob() { return args -> { try { jobLauncher.run(productJob, new JobParametersBuilder() .addLong(<font>"time", System.currentTimeMillis()) .toJobParameters()); } catch (Exception e) { // handle exception<i> } }; }
|
在此示例中,我們建立一個CommandLineRunner bean 來在應用程式啟動時執行該作業。這將使用JobLauncher呼叫productJob。我們還新增了帶有時間戳的唯一JobParameters,以確保該作業每次都以唯一方式執行。測試複合專案閱讀器
為了確保CompositeItemReader按預期工作,我們將測試CompositeItemReader的功能,以確保它能從 CSV 和資料庫源正確讀取產品。
準備測試資料
我們首先準備一個包含產品資料的 CSV 檔案,作為CompositeItemReader的輸入:
productId,productName,stock,price 101,Apple,50,1.99
|
然後,我們還向產品表中插入一條記錄:@BeforeEach public void setUp() { jdbcTemplate.update(<font>"INSERT INTO products (productid, productname, stock, price) VALUES (?, ?, ?, ?)", 102, "Banana", 30, 1.49); }
|
測試閱讀順序
現在,我們將測試CompositeItemReader,以驗證它是否按正確的順序處理產品,從 CSV 和資料庫源讀取。在此測試中,我們從 CSV 檔案讀取產品,然後從資料庫讀取產品,並斷言這些值符合我們的預期:
@Test public void givenTwoReaders_whenRead_thenProcessProductsInOrder() throws Exception { StepExecution stepExecution = new StepExecution( <font>"testStep", new JobExecution(1L, new JobParameters()), 1L); ExecutionContext executionContext = stepExecution.getExecutionContext(); compositeReader.open(executionContext); Product product1 = compositeReader.read(); assertNotNull(product1); assertEquals(101, product1.getProductId()); assertEquals("Apple", product1.getProductName()); Product product2 = compositeReader.read(); assertNotNull(product2); assertEquals(102, product2.getProductId()); assertEquals("Banana", product2.getProductName()); }
|
使用一個讀取器返回空結果進行測試
在本節中,我們將測試當使用多個讀取器並且其中一個讀取器返回null 時CompositeItemReader的行為。 這一點很重要,以確保CompositeItemReader跳過任何不返回任何資料的讀取器並繼續下一個讀取器,直到找到有效資料:
@Test public void givenMultipleReader_whenOneReaderReturnNull_thenProcessDataFromNextReader() throws Exception { ItemStreamReader<Product> emptyReader = mock(ItemStreamReader.class); when(emptyReader.read()).thenReturn(null); ItemStreamReader<Product> validReader = mock(ItemStreamReader.class); when(validReader.read()).thenReturn(new Product(103L, <font>"Cherry", 20, BigDecimal.valueOf(2.99)), null); CompositeItemReader<Product> compositeReader = new CompositeItemReader<>( Arrays.asList(emptyReader, validReader)); Product product = compositeReader.read(); assertNotNull(product); assertEquals(103, product.getProductId()); assertEquals("Cherry", product.getProductName()); }
|