7.1.3 Cập nhật dữ liệu trên máy chủ
Trước khi bạn viết bất kỳ mã controller nào để xử lý các lệnh HTTP PUT hoặc PATCH, bạn nên dành một chút thời gian để xem xét câu hỏi lớn: tại sao lại có hai phương thức HTTP khác nhau để cập nhật tài nguyên?
Mặc dù đúng là PUT thường được dùng để cập nhật dữ liệu tài nguyên, nhưng thực chất nó có ý nghĩa ngữ nghĩa đối lập với GET. Trong khi các yêu cầu GET dùng để chuyển dữ liệu từ máy chủ đến client, thì các yêu cầu PUT dùng để gửi dữ liệu từ client đến máy chủ.
Theo nghĩa đó, PUT thực sự được dùng để thực hiện thao tác thay thế toàn bộ hơn là cập nhật từng phần. Ngược lại, mục đích của HTTP PATCH là thực hiện một bản vá hoặc cập nhật từng phần dữ liệu của tài nguyên.
Ví dụ, giả sử bạn muốn thay đổi địa chỉ trên một đơn hàng. Một cách mà chúng ta có thể thực hiện điều này thông qua REST API là sử dụng yêu cầu PUT được xử lý như sau:
@PutMapping(path="/{orderId}", consumes="application/json")
public TacoOrder putOrder(
@PathVariable("orderId") Long orderId,
@RequestBody TacoOrder order) {
order.setId(orderId);
return repo.save(order);
}Điều này có thể hoạt động, nhưng sẽ yêu cầu client gửi toàn bộ dữ liệu đơn hàng trong yêu cầu PUT. Về mặt ngữ nghĩa, PUT có nghĩa là “đặt dữ liệu này tại URL này”, về cơ bản là thay thế bất kỳ dữ liệu nào đã tồn tại. Nếu bất kỳ thuộc tính nào của đơn hàng bị bỏ qua, giá trị của thuộc tính đó sẽ bị ghi đè bằng null. Ngay cả các taco trong đơn hàng cũng cần được gửi kèm theo dữ liệu đơn hàng, nếu không chúng sẽ bị loại bỏ khỏi đơn hàng.
Nếu PUT thực hiện việc thay thế toàn bộ dữ liệu tài nguyên, vậy làm thế nào để bạn xử lý các yêu cầu chỉ cập nhật từng phần? Đó là lúc HTTP PATCH và annotation @PatchMapping của Spring phát huy tác dụng. Dưới đây là cách bạn có thể viết một phương thức controller để xử lý yêu cầu PATCH cho một đơn hàng:
@PatchMapping(path="/{orderId}", consumes="application/json")
public TacoOrder patchOrder(@PathVariable("orderId") Long orderId,
@RequestBody TacoOrder patch) {
TacoOrder order = repo.findById(orderId).get();
if (patch.getDeliveryName() != null) {
order.setDeliveryName(patch.getDeliveryName());
}
if (patch.getDeliveryStreet() != null) {
order.setDeliveryStreet(patch.getDeliveryStreet());
}
if (patch.getDeliveryCity() != null) {
order.setDeliveryCity(patch.getDeliveryCity());
}
if (patch.getDeliveryState() != null) {
order.setDeliveryState(patch.getDeliveryState());
}
if (patch.getDeliveryZip() != null) {
order.setDeliveryZip(patch.getDeliveryState());
}
if (patch.getCcNumber() != null) {
order.setCcNumber(patch.getCcNumber());
}
if (patch.getCcExpiration() != null) {
order.setCcExpiration(patch.getCcExpiration());
}
if (patch.getCcCVV() != null) {
order.setCcCVV(patch.getCcCVV());
}
return repo.save(order);
}Điều đầu tiên cần lưu ý ở đây là phương thức patchOrder() được đánh dấu bằng @PatchMapping thay vì @PutMapping, cho biết rằng nó nên xử lý các yêu cầu HTTP PATCH thay vì PUT.
Nhưng điều bạn chắc chắn nhận thấy là phương thức patchOrder() phức tạp hơn một chút so với putOrder(). Đó là bởi vì các annotation ánh xạ của Spring MVC, bao gồm @PatchMapping và @PutMapping, chỉ định những loại yêu cầu mà một phương thức nên xử lý. Những annotation này không quy định cách yêu cầu sẽ được xử lý. Mặc dù PATCH theo ngữ nghĩa có nghĩa là cập nhật từng phần, nhưng bạn cần phải viết mã trong phương thức handler để thực sự thực hiện việc cập nhật đó.
Trong trường hợp của phương thức putOrder(), bạn chấp nhận toàn bộ dữ liệu của một đơn hàng và lưu nó lại, phù hợp với ngữ nghĩa của HTTP PUT. Nhưng để patchMapping() tuân theo ngữ nghĩa của HTTP PATCH, phần thân của phương thức cần nhiều logic hơn. Thay vì thay thế toàn bộ đơn hàng bằng dữ liệu mới được gửi đến, nó kiểm tra từng trường của đối tượng TacoOrder được gửi đến và áp dụng bất kỳ giá trị nào khác null vào đơn hàng hiện tại. Cách tiếp cận này cho phép client chỉ gửi các thuộc tính cần thay đổi và máy chủ sẽ giữ nguyên dữ liệu hiện có cho các thuộc tính không được chỉ định bởi client.
Có nhiều cách để PATCH
Cách tiếp cận patch được áp dụng trong phương thức
patchOrder()có những hạn chế sau:
- Nếu giá trị
nullđược hiểu là không thay đổi, thì làm thế nào client có thể chỉ định rằng một trường nên được đặt thànhnull?- Không có cách nào để thêm hoặc xóa một phần tử khỏi một collection. Nếu client muốn thêm hoặc xóa một phần tử, nó phải gửi toàn bộ collection đã được chỉnh sửa.
Thực sự không có quy tắc cố định nào về cách các yêu cầu
PATCHnên được xử lý hoặc dữ liệu gửi đến nên trông như thế nào. Thay vì gửi dữ liệu domain thực tế, client có thể gửi một mô tả cụ thể về patch chứa các thay đổi cần được áp dụng. Tất nhiên, handler của yêu cầu sẽ phải được viết để xử lý các hướng dẫn patch thay vì dữ liệu domain.
Trong cả @PutMapping và @PatchMapping, hãy lưu ý rằng đường dẫn yêu cầu tham chiếu đến tài nguyên cần thay đổi. Đây cũng là cách mà các phương thức được đánh dấu với @GetMapping xử lý đường dẫn.
Giờ bạn đã thấy cách lấy và gửi tài nguyên bằng @GetMapping và @PostMapping. Và bạn cũng đã thấy hai cách khác nhau để cập nhật một tài nguyên bằng @PutMapping và @PatchMapping. Tất cả những gì còn lại là xử lý các yêu cầu để xóa một tài nguyên.
