在Java REST API中,使用PATCH和PUT更新实体的区别。

7

我将要开始用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#)中使用它的任何好处。它似乎只增加了更多的复杂性。


2
似乎状态有效,在 PATCH 请求之后,您的数据库查询应该显示行:“可选地址行2”,因为您正在检查 partialAddress.getLine2() != null。 - kuhajeyan
1
补丁请求应包含客户端计算的指令,服务器可以使用这些指令将某些资源的状态从A转换为B,而不仅仅是简化的部分更新。进一步阅读:[SO documentation](http://stackoverflow.com/documentation/http/3423/http-for-apis/11812/edit-a-resource#t=201610031245323472567)和[good blog post](http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/) - Roman Vottner
这是一个很好的问题。关于第二行中的null是因为您手动映射字段并检查非空值以进行覆盖。目前我正在开发一个项目,我们使用Dozer,您可以选择映射null。无论如何,我们使用POST添加/创建资源,并使用PUT修改它们,但是使用此映射程序可以进行部分映射。所以我在考虑是否应该使用POST(创建),PUT(完全更新),PATCH(部分更新)作为首选方式。 - Shaun McCready
1个回答

7
使用 PATCH 来上传修改后的现有对象几乎总是会出现问题,正如您所提到的那样。如果您想在 JSON 中使用 PATCH,我强烈建议您遵循 RFC 6902RFC 7396 中的一个。我不会谈论 7396,因为我对它不是很熟悉,但是如果要遵循 6902,则需要为 PATCH 操作定义一个单独的资源。在您给出的示例中,它应该是这样的:
PATCH http://localhost:8080/addresses/1
[
    { "op": "replace", "path": "/line1", "value": "1234 NewAddressDownTheStreet ST" },
    { "op": "remove", "path": "/line2" }
]

您可以按照以下步骤进行操作:首先,您需要处理此内容,并创建一个新的实体对象,该对象从当前服务器状态开始并应用中的更改。对新实体对象进行验证。如果通过,则将其推送到数据层。如果失败,则返回错误代码。
如果不会增加太多开销,那么这是一个好主意。幂等性是一件好事。权衡之处在于,您正在通过网络传输更多数据。如果您的资源不大且访问频率不高,则可能没什么大问题。如果您的资源很大且经常访问,则可能会增加显着的开销。当然,我们无法告诉您的临界点。
您似乎完全将资源模型与数据库模型绑定在一起。对于非平凡项目,良好的数据库表设计和良好的资源设计通常看起来非常不同。我知道许多框架会驱使您朝着这个方向发展,但是如果您没有认真考虑过解耦它们,那么您可能需要考虑一下。

我看到的SpringDataRest示例让我认为它只是一个简单的JSON负载,但你列出的请求格式对我来说更有意义。如果我使用我的@service作为事务边界,那么这个Address dbData = getId()并在其上应用更改可能需要在ServiceImpl中进行(而不是在控制器中)。有趣的答案。但现在将保持此问题开放以查看其他建议。 - SGB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接