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 更新资源的两种不同方法,剩下的工作就是处理删除资源的请求。

results matching ""

    No results matching ""