【问题标题】:In a java REST API, using PATCH vs PUT to update an entity在 java REST API 中,使用 PATCH 与 PUT 更新实体
【发布时间】:2017-02-10 00:04:35
【问题描述】:

我即将开始用 Java 开发一个新的 rest api。 我的问题是关于 PATCH 的使用 - 为什么?

假设我们有一个名为 Address.java 的实体

public class Address {

    @Id
    private Long id

    @NotNull
    private String line1;

    private String line2;       //optional

    @NotNull
    private String city;

    @NotNull
    private String state;   
}

要创建一个新地址,我会执行以下 http 请求:

POST http://localhost:8080/addresses

提出以下要求:

{
    "line1" : "mandatory Address line 1",
    "line2" : "optional  Address line 2",
    "city"  : "mandatory City",
    "state" : "cd"
}

假设创建的记录的 id 为 1

对应的@RestController AddressResource.java会有这个方法:

@PostMapping(value = "/addresses")
public ResponseEntity<Address> create(@valid Address newAddress) {
    addressRepo.save(newAddress);
}

@valid 将确保实体在将数据存储到表之前是有效的。

现在假设,我从上面的公寓搬到街对面的房子。如果我使用 PATCH,它就变成了

PATCH http://localhost:8080/addresses/1

带有请求负载:

{
    "line1" : "1234 NewAddressDownTheStreet ST",
    "line2" : null
}

对应的@RestController 方法是:

@PatchMapping(value = "/addresses/{id}")
public ResponseEntity<Address> patchAddress(@PathVariable Long id, Address partialAddress) 
{
    Address dbAddress = addressRepo.findOne(id);
    if (partialAddress.getLine1() != null) {
        dbAddress.setLine1(partialAddress.getLine1());
    }
    if (partialAddress.getLine2() != null) {
        dbAddress.setLine2(partialAddress.getLine2());
    }
    if (partialAddress.getCity() != null) {
        dbAddress.setCity(partialAddress.getCity());
    }
    if (partialAddress.getState() != null) {
        dbAddress.setState(partialAddress.getState());
    }

    addressRepo.save(dbAddress)
}

现在如果你查询表,我的地址不会是吗?

"line1" : "1234 NewAddressDownTheStreet ST",
"line2" : "optional  Address line 2",       <-- INCORRECT. Should be null.
"city"  : "mandatory City",
"state" : "cd"

可以看出,上述更新导致 line2 的值不正确。 这是因为在 java 中,当一个类被实例化时,Address 类中的所有实例变量都被初始化为 null(或默认初始值,如果它们是原始值)。所以没有办法区分 line2 被从默认值更改为 null。

问题 1) 是否有解决此问题的标准方法?


另一个缺点是,我不能使用@Valid 注释在入口点验证请求 - 因为它只是部分的。因此,无效数据可能会进入系统。

例如,假设有具有以下定义的附加字段:

@Min(0) 
@Max(100)
private Integer lengthOfResidencyInYears, 

而且用户不小心输入了 190(当他们真正的意思是 19 年时),它不会失败。


如果我使用 PUT,客户端将需要发送完整的地址对象,而不是 PATCH。 这样做的好处是我可以使用@Valid 来确保地址确实有效


如果假设在进行任何更新之前必须始终执行 GET,那么为什么不使用 PUT 而不是 PATCH? 我错过了什么吗?

一边

我的结论是,使用动态类型语言的开发人员是使用 PATCH 的支持者,因为我看不出从静态类型语言 Java 或 C# 中使用它有什么好处。它似乎增加了更多的复杂性。

【问题讨论】:

  • 似乎状态有效,在 PATCH 请求后,您的数据库查询应显示行:“可选地址行 2”,因为您正在检查,partialAddress.getLine2() != null。
  • 补丁请求应该包含客户端计算的指令,服务器可以使用该指令将某些资源的状态 A 转换为状态 B,而不仅仅是简化的部分更新。进一步阅读:SO documentationgood blog post
  • 这是个好问题。关于 line2 中的 null 正是因为您手动映射字段并检查非 null 值以覆盖。目前我正在做一个我们使用Dozer 的项目,您可以选择映射空值。无论如何,我们使用 POST 添加/创建资源并使用 PUT 修改它们,但是使用此映射器它可以进行部分映射。所以我在争论这是否是首选方式,或者我应该使用 POST(创建)、PUT(完全更新)、PATCH(部分更新)。

标签: java spring rest api backend


【解决方案1】:

由于您概述的原因,使用PATCH 上传现有对象的修改版本几乎总是有问题。如果您想将 PATCH 与 JSON 一起使用,我强烈建议您关注 RFC 6902RFC 7396。我不会与 7396 交谈,因为我不太熟悉它,但要遵循 6902,您将为 PATCH 操作定义一个单独的资源。在您给出的示例中,它看起来像:

PATCH http://localhost:8080/addresses/1
[
    { "op": "replace", "path": "/line1", "value": "1234 NewAddressDownTheStreet ST" },
    { "op": "remove", "path": "/line2" }
]

然后您将对其进行处理,创建一个从当前服务器状态开始的新实体对象,并应用PATCH 中的更改。对新实体对象运行验证。如果通过,则将其推送到数据层。如果失败,返回错误码。

如果PUT 不会增加太多开销,这是个好主意。幂等性是一件好事。权衡是您正在通过网络推送更多数据。如果您的资源不大并且不经常访问,那可能没什么大不了的。如果您的资源很大并且经常被访问,那可能会开始增加大量开销。当然,我们不能告诉你临界点。

您似乎也已将资源模型与数据库模型完全绑定。好的数据库表设计和好的资源设计对于非平凡的项目来说往往看起来很不一样。我知道很多框架都在推动你朝这个方向发展,但如果你还没有认真考虑过将它们解耦,你可能想要这样做。

【讨论】:

  • 我在 SpringDataRest 中看到的示例让我认为这是一个简单的 JSON 有效负载,但您列出的请求格式对我来说更有意义。如果我使用我的@service 作为事务边界,那么这个 Address dbData = getId() 并在顶部应用更改可能需要在 ServiceImpl 中发生(而不是在控制器中)。有趣的答案。但暂时保留这个问题以查看更多建议。
猜你喜欢
  • 2022-12-24
  • 2015-04-12
  • 1970-01-01
  • 2020-10-15
  • 2020-01-09
  • 1970-01-01
  • 2017-01-05
  • 2018-01-08
相关资源
最近更新 更多