Spring中實現面向寫入的批次和批處理API

banq發表於2024-06-14

實現標準 REST API 涵蓋了大多數典型用例。但是,基於 REST 的架構風格在處理任何批次或批處理操作時存在一些限制。

在本教程中,我們將學習如何在微服務中應用批次和批處理操作。此外,我們還將實現一些自定義的面向寫入的批次和批處理 API。

bBulk批次處理和 Batch批處理API 介紹/b
批次bBulk/b和批bBatch/b操作這兩個術語經常被互換使用。不過,兩者之間還是有明顯區別的。

1、通常情況下,批次bBulk/b操作是指對b同一型別/b的多個條目執行相同的操作。為每個請求呼叫相同的應用程式介面來執行批次操作可能是一種平常的方法。這種方法可能太慢,而且會浪費資源。相反,我們可以在一次往返中處理多個條目。

我們可以透過在一次呼叫中對同一型別的多個條目應用相同的操作來實現批次操作。這種對條目集合進行操作的方式可以減少整體延遲,提高應用程式效能。要實施批次操作,我們可以重新使用用於單個條目的現有端點,或者為批次方法建立一個單獨的路由。

2、批處理bBatch/b操作通常意味著在b多個資源型別/b上執行不同的操作。批處理bBatch/b API 是在一次呼叫中對資源執行各種操作的捆綁。這些資源操作可能沒有任何連貫性。每個請求路由都可能獨立於其他路由。

簡而言之,"批處理batch "一詞是指批處理處理不同的請求。

我們並沒有很多定義明確的標準或規範來實現批次bBulk/b或批處理bBatch/b操作。此外,許多流行的框架(如 Spring)也不支援批次bBulk/b操作。

不過,在本教程中,我們將使用常見的 REST 結構自定義實現批次bBulk/b和批處理​​​​​​​bBatch/b處理操作。


bSpring中的示例應用程式/b
假設我們需要構建一個支援批次和批處理操作的簡單微服務。

b1. Maven 依賴項/b
首先,讓我們包含 spring-boot-starter-web和spring-boot-starter-validation依賴項:

code<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>3.1.5</version>
</dependency>/code
透過上述spring-boot-starter-validation依賴項,我們在應用程式中啟用了輸入資料驗證。我們將需要它來驗證批次和批處理請求的大小。

b2. 實現第一個 Spring 服務/b
我們將實現在儲存庫中建立、更新和刪除資料的服務。

首先,讓我們對 Customer 類進行建模:

codepublic class Customer implements Serializable {
    private String id;
    private String name;
    private String email;
    private String address;
    // standard getters and setters
}/code
接下來,讓我們使用createCustomers()方法實現CustomerService 類,以在記憶體儲存庫中儲存多個Customer物件:

code@Service
public class CustomerService {
    private final Map<String, Customer> customerRepoMap = new HashMap<>();
    public List<Customer> createCustomers(List<Customers> customers) {
        return customers.stream()
          .map(this::createCustomer)
          .filter(Optional::isPresent)
          .map(Optional::get)
          .collect(toList());
    }
}/code
然後,我們將實現createCustomer()方法來建立單個Customer物件:

codepublic Optional<Customer> createCustomer(Customer customer) {
    if (!customerRepoMap.containsKey(customer.getEmail()) && customer.getId() == 0) {
        Customer customerToCreate = new Customer(customerRepoMap.size() + 1, 
          customer.getName(), customer.getEmail());
        customerToCreate.setAddress(customer.getAddress());
        customerRepoMap.put(customerToCreate.getEmail(), customerToCreate);  
        return Optional.of(customerToCreate);
    }
    return Optional.empty();
}/code
在上述方法中,如果儲存庫中不存在客戶,我們才會建立客戶,否則,我們返回一個空物件。

類似地,我們將實現一種方法來更新現有的客戶詳細資訊:

codeprivate Optional<Customer> updateCustomer(Customer customer) {
    Customer customerToUpdate = customerRepoMap.get(customer.getEmail());
    if (customerToUpdate != null && customerToUpdate.getId() == customer.getId()) {
        customerToUpdate.setName(customer.getName());
        customerToUpdate.setAddress(customer.getAddress());
    }
    return Optional.ofNullable(customerToUpdate);
}/code
最後,我們將實現一個deleteCustomer()方法來從儲存庫中刪除現有的客戶:

codepublic Optional<Customer> deleteCustomer(Customer customer) {
    Customer customerToDelete = customerRepoMap.get(customer.getEmail());
    if (customerToDelete != null && customerToDelete.getId() == customer.getId()) {
        customerRepoMap.remove(customer.getEmail());
    }
   return Optional.ofNullable(customerToDelete);
}/code

b3. 實現第二個 Spring 服務/b
我們還來實現另一項在儲存庫中獲取和建立地址資料的服務。

首先,我們定義Address 類:

codepublic class Address implements Serializable {
    private int id;
    private String street;
    private String city;
    //standard getters and setters
}/code
然後,讓我們用createAddress()方法實現AddressService 類:

codepublic Address createAddress(Address address) {
    Address createdAddress = null;
    String addressUniqueKey = address.getStreet().concat(address.getCity());
    if (!addressRepoMap.containsKey(addressUniqueKey)) {
        createdAddress = new Address(addressRepoMap.size() + 1, 
          address.getStreet(), address.getCity());
        addressRepoMap.put(addressUniqueKey, createdAddress);
    }
    return createdAddress;
}/code

b使用現有端點實現批次 API/b
現在讓我們建立一個 API 來支援批次和單個專案建立操作。

b1. 實現批次控制器/b
我們將實現一個帶有端點的BulkController類,以便在一次呼叫中建立一個或多個客戶。

首先,我們將以 JSON 格式定義批次請求:

code
    {
        "name": "<name>",
        "email": "<email>",
        "address": "<address>"
    }
/code
透過這種方法,我們可以使用自定義HTTP標頭 - X-ActionType -處理批次操作,以區分批次或單項操作。

然後,我們將在BulkController類中實現bulkCreateCustomers()方法並使用上述CustomerService 的方法:

code@PostMapping(path = "/customers/bulk")
public ResponseEntity<List<Customer>> bulkCreateCustomers(
  @RequestHeader(value="X-ActionType") String actionType, 
  @RequestBody @Valid @Size(min = 1, max = 20) List<Customer> customers) {
    List<Customer> customerList = actionType.equals("bulk") ? 
      customerService.createCustomers(customers) :
      singletonList(customerService.createCustomer(customers.get(0)).orElse(null));
    return new ResponseEntity<>(customerList, HttpStatus.CREATED);
}/code
在上面的程式碼中,我們使用X-ActionType標頭來接受任何批次請求。此外,我們還使用@ Size註釋新增了輸入請求大小驗證。程式碼決定是將整個列表傳遞給 createCustomers()還是僅將元素 0傳遞給 createCustomer()。

不同的建立函式返回一個列表或一個單個的Optional,因此我們將後者轉換為 列表,以便在兩種情況下 HTTP 響應相同。

b2. 驗證批次 API/b
我們將執行應用程式並透過執行上述端點來驗證批次操作:

code$ curl -i --request POST 'http://localhost:8080/api/customers/bulk' \
--header 'X-ActionType: bulk' \
--header 'Content-Type: application/json' \
--data-raw '
    {
        "name": "test1",
        "email": "test1@email.com",
        "address": "address1"
    },
    {
        "name": "test2",
        "email": "test2@email.com",
        "address": "address2"
    }
'/code
當客戶建立完成後,我們將收到以下成功響應:

codeHTTP/1.1 201 
{"id":1,"name":"test1","email":"test1@email.com","address":"address1"},
{"id":1,"name":"test2","email":"test2@email.com","address":"address2"},
.../code
接下來,我們將實現另一種批次操作方法。

b使用不同的端點實現批次 API/b
在批次 API 中對同一資源執行不同的操作並不常見。不過,讓我們看看最靈活的方法,看看如何做到這一點。

我們可以實現原子批次操作,其中整個請求在單個事務中成功或失敗。或者,我們可以允許成功的更新獨立於失敗的更新進行,並透過響應指示它是完全成功還是部分成功。我們將實現其中的第二種。

b1. 定義請求和響應模型/b
讓我們考慮在一次呼叫中建立、更新和刪除多個客戶的用例。

我們將批次請求定義為 JSON 格式:

code
    {
        "bulkActionType": "<CREATE OR UPDATE OR DELETE>",
        "customers":
            {
                "name": "<name>",
                "email": "<email>",
                "address": "<address>"
            }
       
    }
/code
首先,我們將上述 JSON 格式建模到CustomerBulkRequest 類中:

codepublic class CustomerBulkRequest {
    private BulkActionType bulkActionType;
    private List<Customer> customers;
    //standard getters and setters
}/code
接下來,我們將實現BulkActionType 列舉:

codepublic enum BulkActionType {
    CREATE, UPDATE, DELETE
}/code
然後,我們將CustomerBulkResponse 類定義為 HTTP 響應物件:

codepublic class CustomerBulkResponse {
    private BulkActionType bulkActionType;
    private List<Customer> customers;
    private BulkStatus status;
    //standard getters and setters
}/code
最後,我們將定義BulkStatus列舉來指定每個操作的返回狀態:

codepublic enum BulkStatus {
    PROCESSED, PARTIALLY_PROCESSED, NOT_PROCESSED
}/code

b2. 實現批次控制器/b
我們將實現一個批次 API,該 API 接受批次請求並根據 bulkActionType列舉 進行處理 ,然後一起返回批次狀態和客戶資料。

首先,我們將在BulkController類中建立一個EnumMap ,並將BulkActionType 列舉對映 到其自己的CustomerService 的Function:

code@RestController
@RequestMapping("/api")
@Validated
public class BulkController {
    private final CustomerService customerService;
    private final EnumMap<BulkActionType, Function<Customer, Optional<Customer>>> bulkActionFuncMap = 
      new EnumMap<>(BulkActionType.class);
    public BulkController(CustomerService customerService) {
        this.customerService = customerService;
        bulkActionFuncMap.put(BulkActionType.CREATE, customerService::createCustomer);
        bulkActionFuncMap.put(BulkActionType.UPDATE, customerService::updateCustomer);
        bulkActionFuncMap.put(BulkActionType.DELETE, customerService::deleteCustomer);
    }
}/code
此 EnumMap提供請求型別與我們需要滿足的 CustomerService上的方法之間的繫結。它幫助我們避免冗長的switch或if 語句。

我們可以將 EnumMap返回的 針對操作型別 的函式傳遞給Customer 物件流上的 map()方法:

codeList<Customer> customers = customerBulkRequest.getCustomers().stream()
   .map(bulkActionFuncMap.get(customerBulkRequest.getBulkActionType()))
   .../code
由於我們所有的 Function物件都從 Customer對映到 Optional<Customer>,這本質上使用流中的 map()操作來執行批次請求,並將生成的Customer留在流中(如果可用)。

讓我們在完整的控制器方法中將它們放在一起:

code@PostMapping(path = "/customers/bulk")
public ResponseEntity<List<CustomerBulkResponse>> bulkProcessCustomers(
  @RequestBody @Valid @Size(min = 1, max = 20) 
  List<CustomerBulkRequest> customerBulkRequests) {
    List<CustomerBulkResponse> customerBulkResponseList = new ArrayList<>();
    customerBulkRequests.forEach(customerBulkRequest -> {
        List<Customer> customers = customerBulkRequest.getCustomers().stream()
          .map(bulkActionFuncMap.get(customerBulkRequest.getBulkActionType()))
          .filter(Optional::isPresent)
          .map(Optional::get)
          .collect(toList());
        
        BulkStatus bulkStatus = getBulkStatus(customerBulkRequest.getCustomers(), 
          customers);     
        customerBulkResponseList.add(CustomerBulkResponse.getCustomerBulkResponse(customers, 
          customerbulkRequest.getBulkActionType(), bulkStatus));
    });
    return new ResponseEntity<>(customerBulkResponseList, HttpStatus.Multi_Status);
}/code
此外,我們將完成getBulkStatus方法,以根據建立的客戶數量 返回特定的bulkStatus 列舉:

codeprivate BulkStatus getBulkStatus(List<Customer> customersInRequest, 
  List<Customer> customersProcessed) {
    if (!customersProcessed.isEmpty()) {
        return customersProcessed.size() == customersInRequest.size() ?
          BulkStatus.PROCESSED : 
          BulkStatus.PARTIALLY_PROCESSED;
    }
    return BulkStatus.NOT_PROCESSED;
}/code
我們應該注意,還可以考慮新增針對每個操作之間任何衝突的輸入驗證。

b3. 驗證批次 API/b
我們將執行該應用程式並呼叫上述端點,即 /customers/bulk:

code$ curl -i --request POST 'http://localhost:8080/api/customers/bulk' \
--header 'Content-Type: application/json' \
--data-raw '
    {
        "bulkActionType": "CREATE",
        "customers":
            {
                "name": "test4",
                "email": "test4@email.com",
                ...
            }
       
    },
    {
        "bulkActionType": "UPDATE",
        "customers":
            ...
       
    },
    {
        "bulkActionType": "DELETE",
        "customers":
            ...
       
    }
'/code
現在讓我們驗證一下成功的響應:

codeHTTP/1.1 207 
{"customers":{"id":4,"name":"test4","email":"test4@email.com","address":"address4"},"status":"PROCESSED","bulkType":"CREATE"},
.../code
接下來,我們將實現一個批處理 API,它將客戶和地址捆綁在一個批處理呼叫中。

b實現批batch處理 API/b
通常,批次 API 請求是具有其自己的方法、資源 URL 和有效負載的子請求的集合。

我們將實現一個批處理 API,用於建立和更新兩種資源型別。當然,我們可以包含其他操作,例如刪除操作。但為簡單起見,我們僅考慮 POST和PATCH方法。

b1. 實現批請求模型/b
首先,我們將以 JSON 格式定義混合資料請求模型:

code
    {
        "method": "POST",
        "relativeUrl": "/address",
        "data": {
            "street": "<street>",
            "city": "<city>"
        }
    },
    {
        "method": "PATCH",
        "relativeUrl": "/customer",
        "data": {
            "id": "<id>",
            "name": "<name>",
            "email": "<email>",
            "address": "<address>"
        }
    }
/code
我們將把上述 JSON 結構實現為BatchRequest 類:

codepublic class BatchRequest {
    private HttpMethod method;
    private String relativeUrl;
    private JsonNode data;
    //standard getters and setters
}/code

b2. 實現批處理控制器/b
我們將實現一個批處理 API,以便在單個請求中建立地址並向客戶更新其地址。為簡單起見,我們將在同一個微服務中編寫此 API。在另一種架構模式中,我們可能選擇在並行呼叫各個端點的其他微服務中實現它。

使用上面的BatchRequest類,我們將遇到將JsonNode反序列化為特定型別類的問題。我們可以透過使用ObjectMapper 的convertValue方法將JsonNode轉換為強型別物件來輕鬆解決這個問題。

對於批處理 API,我們將根據BatchRequest 類中請求的HttpMethod和relatedUrl引數呼叫AddressService或CustomerService方法。

 我們將在BatchController 類中實現批處理端點:

code@PostMapping(path = "/batch")
public String batchUpdateCustomerWithAddress(
  @RequestBody @Valid @Size(min = 1, max = 20) List<BatchRequest> batchRequests) {
    batchRequests.forEach(batchRequest -> {
        if (batchRequest.getMethod().equals(HttpMethod.POST) && 
          batchRequest.getRelativeUrl().equals("/address")) {
            addressService.createAddress(objectMapper.convertValue(batchRequest.getData(), 
              Address.class));
        } else if (batchRequest.getMethod().equals(HttpMethod.PATCH) && 
            batchRequest.getRelativeUrl().equals("/customer")) {
            customerService.updateCustomer(objectMapper.convertValue(batchRequest.getData(), 
              Customer.class));
        }
    });
    return "Batch update is processed";
}/code

b3. 驗證批處理 API/b
我們將執行上述/batch端點:

code$ curl -i --request POST 'http://localhost:8080/api/batch' \
--header 'Content-Type: application/json' \
--data-raw '
    {
        "method": "POST",
        "relativeUrl": "/address",
        "data": {
            "street": "test1",
            "city": "test"
        }
    },
    {
        "method": "PATCH",
        "relativeUrl": "/customer",
        "data": {
            "id": "1",
            "name": "test1",
            "email": "test1@email.com",
            "address": "address2"
        }
    }
'/code
我們將驗證以下回應:

codeHTTP/1.1 200
Batch update is processed/code

b結論/b
在本文中,我們學習瞭如何在 Spring 應用程式中應用批操作。我們還了解了它們的功能和區別。

對於批次bulk操作,我們在兩個不同的 API 中實現了它,一個是重用現有的POST端點來建立多個資源,另一個是建立單獨的端點以允許對同一型別的多個資源進行多個操作。

我們還實現了批batch處理 API,允許我們將不同的操作應用於不同的資源。批處理 API 使用 HttpMethod和relativeUrl以及有效負載組合不同的子請求。
 

相關文章