7.1.3 更新服务器上的数据
在编写任何处理 HTTP PUT 或 PATCH 命令的控制器代码之前,应该花点时间考虑一下这个问题:为什么有两种不同的 HTTP 方法来更新资源呢?
虽然 PUT 经常用于更新资源数据,但它实际上是 GET 语义的对立面。GET 请求用于将数据从服务器传输到客户机,而 PUT 请求用于将数据从客户机发送到服务器。
从这个意义上说,PUT 实际上是用于执行大规模替换操作,而不是更新操作。相反,HTTP PATCH 的目的是执行补丁或部分更新资源数据。
例如,假设希望能够更改订单上的地址,我们可以通过 REST API 实现这一点,可以用以下这种方式处理 PUT 请求:
@PutMapping(path="/{orderId}", consumes="application/json")
public TacoOrder putOrder(
@PathVariable("orderId") Long orderId,
@RequestBody TacoOrder order) {
order.setId(orderId);
return repo.save(order);
}
这可能行得通,但它要求客户端在 PUT 请求中提交完整的订单数据。从语义上讲,PUT 的意思是“把这个数据放到这个 URL 上”,本质上是替换任何已经存在的数据。如果订单的任何属性被省略,该属性的值将被 null 覆盖。甚至订单中的 taco 也需要与订单数据一起设置,否则它们将从订单中删除。
如果 PUT 完全替换了资源数据,那么应该如何处理只进行部分更新的请求?这就是 HTTP PATCH 请求和 Spring 的 @PatchMapping
的好处。可以这样写一个控制器方法来处理一个订单的 PATCH 请求:
@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);
}
这里要注意的第一件事是,patchOrder()
方法是用 @PatchMapping
而不是 @PutMapping
来注解的,这表明它应该处理 HTTP PATCH 请求而不是 PUT 请求。
但是 patchOrder()
方法比 putOrder()
方法更复杂一些。这是因为 Spring MVC 的映射注解(包括 @PatchMapping
和 @PutMapping
)只指定了方法应该处理哪些类型的请求。这些注解没有规定如何处理请求。尽管 PATCH 在语义上暗示了部分更新,但是可以在处理程序方法中编写实际执行这种更新的代码。
对于 putOrder()
方法,接受订单的完整数据并保存它,这符合 HTTP PUT 的语义。但是为了使 patchMapping()
坚持 HTTP PATCH 的语义,该方法的主体需要更多语句。它不是用发送进来的新数据完全替换订单,而是检查传入订单对象的每个字段,并将任何非空值应用于现有订单。这种方法允许客户机只发送应该更改的属性,并允许服务器为客户机未指定的任何属性保留现有数据。
使用 PATCH 的方法不止一种
PATCH 方式应用于
patchOrder()
方法时,有两个限制:
- 如果传递的是 null 值,意味着没有变化,那么客户端如何指示字段应该设置为 null?
- 没有办法从一个集合中移除或添加一个子集。如果客户端想要从集合中添加或删除一条数据,它必须发送完整的修改后的集合。
对于应该如何处理 PATCH 请求或传入的数据应该是什么样子,确实没有硬性规定。客户端可以发送应用于特定 PATCH 请求的描述,这个描述包含着需要被应用于数据的更改,而不是发送实际的域数据。当然,必须编写请求处理程序来处理 PATCH 指令,而不是域数据。
在 @PutMapping
和 @PatchMapping
中,请注意请求路径引用了将要更改的资源。这与 @GetMappingannotated
方法处理路径的方式相同。
现在已经了解了如何使用 @GetMapping
和 @PostMapping
来获取和发布资源。已经看到了使用 @PutMapping
和 @PatchMapping
更新资源的两种不同方法,剩下的工作就是处理删除资源的请求。