HTTP将VS vs HTTP补丁放在REST API中
1. 概述
在这个快速教程中,我们将研究HTTP PUT 和 PATCH 之间的差异以及这两个操作的语义。
我们将使用 Spring 实现两个支持这两种类型操作的 REST 端点,以便更好地理解差异和正确使用它们的方法。
2. 什么时候用Put,什么时候Patch?
让我们从一个简单的和稍微简单的语句开始。
当客户端需要完全替换现有资源时,他们可以使用 PUT。当他们进行部分更新时,他们可以使用 HTTP PATCH。
例如,在更新 Resource 的单个字段时,发送完整的 Resource 表示可能很麻烦,并且会占用大量不必要的带宽。在这种情况下,PATCH 的语义更有意义。
这里要考虑的另一个重要方面是**幂等性。PUT 是幂等的;PATCH 可以是幂等的,但不是必须的。**所以,根据我们正在实现的操作的语义,我们也可以根据这个特性选择一个或另一个。
3. 实现 PUT 和 PATCH 逻辑
假设我们要实现 REST API 来更新具有多个字段的HeavyResource:
public class HeavyResource {
private Integer id;
private String name;
private String address;
// ...
首先,我们需要创建使用 PUT 处理资源完整更新的端点:
@PutMapping("/heavyresource/{id}")
public ResponseEntity<?> saveResource(@RequestBody HeavyResource heavyResource,
@PathVariable("id") String id) {
heavyResourceRepository.save(heavyResource, id);
return ResponseEntity.ok("resource saved");
}
这是更新资源的标准端点。
现在假设地址字段经常由客户端更新。在这种情况下,我们不想发送带有所有字段的整个HeavyResource对象,但我们确实希望能够只更新address字段——通过 PATCH 方法。
我们可以创建一个HeavyResourceAddressOnly DTO 来表示地址字段的部分更新:
public class HeavyResourceAddressOnly {
private Integer id;
private String address;
// ...
}
接下来,我们可以利用 PATCH 方法发送部分更新:
@PatchMapping("/heavyresource/{id}")
public ResponseEntity<?> partialUpdateName(
@RequestBody HeavyResourceAddressOnly partialUpdate, @PathVariable("id") String id) {
heavyResourceRepository.save(partialUpdate, id);
return ResponseEntity.ok("resource address updated");
}
有了这个更细粒度的 DTO,我们可以只发送我们需要更新的字段,而无需发送整个HeavyResource的开销。 如果我们有大量这样的部分更新操作,我们也可以跳过为每个输出创建自定义 DTO ——并且只使用一个映射:
@RequestMapping(value = "/heavyresource/{id}", method = RequestMethod.PATCH, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> partialUpdateGeneric(
@RequestBody Map<String, Object> updates,
@PathVariable("id") String id) {
heavyResourceRepository.save(updates, id);
return ResponseEntity.ok("resource updated");
}
这个解决方案将让我们在实现 API 时更加灵活,但我们也确实丢失了一些东西,例如验证。
4. 测试 PUT 和 PATCH
最后,让我们为这两种 HTTP 方法编写测试。
首先,我们要通过 PUT 方法测试完整资源的更新:
mockMvc.perform(put("/heavyresource/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(
new HeavyResource(1, "Tom", "Jackson", 12, "heaven street")))
).andExpect(status().isOk());
部分更新的执行是通过使用 PATCH 方法实现的:
mockMvc.perform(patch("/heavyrecource/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(
new HeavyResourceAddressOnly(1, "5th avenue")))
).andExpect(status().isOk());
我们还可以为更通用的方法编写测试:
HashMap<String, Object> updates = new HashMap<>();
updates.put("address", "5th avenue");
mockMvc.perform(patch("/heavyresource/1")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(updates))
).andExpect(status().isOk());
5. 处理null值的部分请求
当我们为 PATCH 方法编写实现时,我们需要指定一个约定,当我们将null作为HeavyResourceAddressOnly中的address字段的值时如何处理这种情况。 假设客户端发送以下请求:
{
"id" : 1,
"address" : null
}
然后我们可以将其处理为将address字段的值设置为null或通过将其视为无更改来忽略此类请求。
我们应该选择一种处理null的策略,并在每个 PATCH 方法实现中坚持下去。