1. 概述

本文快速解析 HTTP PUT 和 PATCH 两种动词的语义差异,并对比它们在 REST API 中的实际应用场景。我们将通过 Spring 框架实现两个支持这两种操作的接口,帮助开发者更清晰地理解它们的区别及正确使用方式。

2. 何时使用 PUT,何时使用 PATCH?

简单粗暴地说:

  • PUT:当客户端需要完全替换现有资源时使用
  • PATCH:当客户端只需要部分更新资源时使用

举个实际场景:如果只需要更新资源的单个字段(如地址),发送完整的资源表示会显得笨拙且浪费带宽。这种情况下,PATCH 的语义显然更合适。

另一个关键差异是幂等性

  • PUT 是幂等的:多次执行同一操作结果不变
  • ⚠️ PATCH 可能非幂等:取决于具体实现(非强制要求)

根据业务操作的幂等需求,这也是选择方法的重要依据。

3. 实现 PUT 和 PATCH 逻辑

假设我们要更新一个包含多个字段的 HeavyResource

public class HeavyResource {
    private Integer id;
    private String name;
    private String address;
    // ...

3.1 PUT 实现完整更新

@PutMapping("/heavyresource/{id}")
public ResponseEntity<?> saveResource(@RequestBody HeavyResource heavyResource,
  @PathVariable("id") String id) {
    heavyResourceRepository.save(heavyResource, id);
    return ResponseEntity.ok("resource saved");
}

这是标准的资源更新接口实现。

3.2 PATCH 实现部分更新

address 字段需要频繁更新时,我们不想每次都发送完整的 HeavyResource 对象。这时可以:

  1. 创建专用 DTO(仅包含需更新字段):

    public class HeavyResourceAddressOnly {
     private Integer id;
     private String address;
     
     // ...
    }
    
  2. 实现 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 只发送需要更新的字段,避免了传输完整对象的开销。

3.3 通用 PATCH 方案

当有大量部分更新操作时,可以跳过创建专用 DTO,直接使用 Map:

@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");
}

⚠️ 注意:这种方案更灵活,但会失去数据校验能力(如 DTO 的注解验证)。

4. 测试 PUT 和 PATCH

4.1 测试 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());

4.2 测试 PATCH 部分更新

mockMvc.perform(patch("/heavyrecource/1")
  .contentType(MediaType.APPLICATION_JSON_VALUE)
  .content(objectMapper.writeValueAsString(
    new HeavyResourceAddressOnly(1, "5th avenue")))
  ).andExpect(status().isOk());

4.3 测试通用 PATCH

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 时需明确约定:当收到 address 字段为 null 的请求时如何处理?例如:

{
   "id" : 1,
   "address" : null
}

可选策略:

  • 显式置空:将字段值设为 null
  • 忽略更新:视为无变更操作

⚠️ 重要:必须在所有 PATCH 实现中保持一致的 null 处理策略,避免踩坑。

6. 总结

本文深入对比了 HTTP PATCH 和 PUT 的核心差异:

  • PUT:完整资源替换,强制幂等
  • PATCH:部分资源更新,非强制幂等

我们通过 Spring 实现了:

  1. 使用 PUT 的完整资源更新接口
  2. 使用 PATCH 的部分更新接口(专用 DTO 和通用 Map 两种方案)

所有示例代码可在 GitHub 项目 中获取(Maven 项目,可直接导入运行)。


原始标题:HTTP PUT vs HTTP PATCH in a REST API