如果我还要更新一个时间戳属性,我应该使用PUT方法来进行更新吗?

33
根据REST风格,通常认为HTTP POST、GET、PUT和DELETE方法应该用于创建、读取、更新和删除(CRUD)操作。但是如果我们坚持使用HTTP方法定义,可能并不那么清晰。在这篇文章中解释了:简而言之:仅当您知道资源将位于的URL和资源的全部内容时,请使用PUT。否则,请使用POST。主要是因为PUT是一个更加限制性的动词。它接受完整的资源并将其存储在给定的URL处。如果以前有资源存在,则会替换它;如果没有,则会创建新的资源。这些属性支持幂等性,而天真的创建或更新操作可能没有。我怀疑这可能是PUT被定义为它的方式的原因;它是一种幂等操作,允许客户端向服务器发送信息。在我的情况下,我通常传递所有资源数据来进行更新,所以我可以在更新时使用PUT,但每次我发出更新时,我都会保存一个LastUser和LastUpdate列,其中包含进行修改的用户ID和操作时间。我想知道您的意见,因为严格来说,这两个列不是资源的一部分,但它们确实防止了操作的幂等性。

你如何表示LastUserLastUpdate - 它们是否是资源表示的一部分(即XML中的节点)? - MicE
不,当发布更新时它们甚至不存在,但是在使用GET查询时会返回它们...所以我先进行PUT,然后进行GET,获取最后更新时间,再次执行相同的PUT,另一个GET会产生不同的lastUpdate... - opensas
好的,谢谢确认 - 请看下面我的答案,提供了另一种解决问题的方法。 - MicE
4个回答

32

除了关于REST风格将CRUD映射到HTTP方法的评论之外,这是一个非常好的问题。

你的问题的答案是,是的,在这种情况下你可以自由地使用PUT方法,即使有些资源元素以不幂等的方式被服务器更新。不幸的是,这个答案背后的推理相当模糊。重要的是要理解客户端请求的意图。客户端希望使用传递的值完全替换资源的内容。客户端不负责服务器执行其他操作,因此不会违反HTTP方法的语义。

这就是允许服务器在进行GET操作时更新页面计数器的推理。客户端没有要求更新,因此GET是安全的,即使服务器选择进行更新。

整个完整资源与部分资源的辩论最终已经在更新的HTTP规范中详细说明。

由于可能被解释为部分内容(或者可能是被错误地放置为完整表现的部分内容),源服务器应拒绝包含Content-Range头字段的任何PUT请求。通过定位与重叠该较大资源一部分的状态的另一个标识资源,或使用专门针对部分更新的不同方法(例如[RFC5789]中定义的PATCH方法),可以进行部分内容更新。

所以,我们现在知道应该做什么。不太清楚的是为什么只允许发送完整响应的限制存在。这个问题曾被问过,但我认为在rest-discuss上仍未得到解答。


是的,我同意。不过,使用PATCH或Content-Range头部感觉不太合适,因为客户端试图修改整个资源(至少是客户端被允许修改的部分)。请参考我的答案,对这个问题有一个不同的看法。我知道这也不是百分之百可靠的解决方案,但至少它试图减轻问题的影响。 - MicE
2
我并不是在称呼任何人为白痴,但这篇文章很有用。http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/ - backdesk
同意答案。值得注意的是,RFC7231还涉及到服务器更新资源的元素的相关部分(顺便说一句,它是RFC2616的一个过时的RFC之一)。它在这里提到,服务器可以对主体应用转换,并描述了一种机制,使客户端“允许用户代理知道其内存中的表示体仍然是当前的”,即:服务器“必须不发送... ETag或Last-Modified...除非请求的表示形式没有经过任何转换保存”。 - Slawomir Brzezinski

9

由于客户端无法修改LastUserLastUpdate,因此我建议从资源的表现形式中完全删除它们。让我用一个例子来解释我的推理。

假设我们的典型API在要求提供单个资源时将向客户端返回以下表示:

GET /example/123

<?xml version="1.0" encoding="UTF-8" ?>
<example>
    <id>123</id>
    <lorem>ipsum</lorem>
    <dolor>sit amet</dolor>
    <lastUser uri="/user/321">321</lastUser>
    <lastUpdate>2011-04-16 20:00:00 GMT</lastUpdate>
</example>

如果客户端想要修改资源,它通常会获取整个表现形式并将其发送回API。
PUT /example/123

<?xml version="1.0" encoding="UTF-8" ?>
<example>
    <id>123</id>
    <lorem>foobar</lorem>
    <dolor>foobaz</dolor>
    <lastUser>322</lastUser>
    <lastUpdate>2011-04-16 20:46:15 GMT+2</lastUpdate>
</example>

由于API自动生成lastUserlastUpdate的值,无法接受客户端提供的数据。因此,最合适的响应应该是400 Bad Request403 Forbidden(因为客户端无法修改这些值)。
如果我们想要符合REST标准,并在进行PUT请求时发送资源的完整表示,我们需要从资源的表示中删除lastUserlastUpdate。这将使客户端能够通过PUT发送完整的实体:
PUT /example/123

<?xml version="1.0" encoding="UTF-8" ?>
<example>
    <id>123</id>
    <lorem>foobar</lorem>
    <dolor>foobaz</dolor>
</example>

服务器现在将接受一个完整的表示,因为它不包含lastUpdatelastUser
剩下的问题是如何让客户端访问lastUpdatelastUser。如果他们不需要它(并且这些字段仅在API内部需要),那么我们很好,我们的解决方案完全符合RESTful的要求。然而,如果客户端需要访问这些数据,最干净的方法是使用HTTP头:
GET /example/123

...
Last-Modified: Sat, 16 Apr 2011 18:46:15 GMT
X-Last-User: /user/322
...

<?xml version="1.0" encoding="UTF-8" ?>
<example>
    <id>123</id>
    <lorem>foobar</lorem>
    <dolor>foobaz</dolor>
</example>

使用自定义HTTP头并不理想,因为需要教导用户代理程序如何读取它。如果我们希望以更简单的方式为客户端提供访问相同数据的方式,唯一能做的就是将数据放入表示中,这时我们面临着与原始问题相同的问题。我至少会尝试以某种方式缓解这个问题。如果API使用的内容类型是XML,我们可以将数据放入节点属性中,而不是直接将其作为节点值公开,即:

GET /example/123

...
Last-Modified: Sat, 16 Apr 2011 18:46:15 GMT
...

<?xml version="1.0" encoding="UTF-8" ?>
<example last-update="2011-04-16 18:46:15 GMT" last-user="/user/322">
    <id>123</id>
    <lorem>foobar</lorem>
    <dolor>foobaz</dolor>
</example>

这样,我们至少可以避免客户端尝试在后续的PUT请求中提交所有XML节点的问题。这对于JSON是行不通的,解决方案仍然有点靠近幂等性的边缘(因为API在处理请求时仍然必须忽略XML属性)。

更好的做法是,正如Jonah在评论中指出的那样,如果客户端需要访问lastUserlastUpdate,这些可以作为一个新资源公开,从原始资源中链接,例如:

GET /example/123

<?xml version="1.0" encoding="UTF-8" ?>
<example>
    <id>123</id>
    <lorem>foobar</lorem>
    <dolor>foobaz</dolor>
    <lastUpdateUri>/example/123/last-update</lastUpdateUri>
</example>

...然后:

GET /example/123/last-update

<?xml version="1.0" encoding="UTF-8" ?>
<lastUpdate>
    <resourceUri>/example/123</resourceUri>
    <updatedBy uri="/user/321">321</updatedBy>
    <updatedAt>2011-04-16 20:00:00 GMT</updatedAt>
</lastUpdate>

以上内容也可以很好地扩展,提供完整的审计日志和个别更改,前提是有可用的资源更改日志。

请注意:
我同意Darrel Miller问题上的看法,但我想提供一个不同的解决方案。请注意,这种方法没有任何标准/RFC等支持,只是对该问题的不同看法。


感谢提供另一种方法。使用“最后修改”头似乎是一个相当不错的想法,特别是当涉及到缓存或拒绝过时更新(即在您忙于更改它们时被其他人编辑的资源)时。有什么想法? - backdesk
1
为什么不在一个新的端点上公开它呢?毕竟,正如您所说服人的那样,它是与原始资源分开的概念:example/123/lastUpdate - Jonah
@Jonah - 非常好的观点,谢谢。我更新了答案以纳入它。(请注意,例如编辑中的URI可能更适合作为XML节点属性。当前格式应允许在需要时更直接地转换为JSON。) - MicE
关于 "这种方法没有任何标准/ RFC支持" 的说法,@MicE,有好消息。事实上,它由RFC支持。2014年的RFC 7231(其中之一废弃了1999年的RFC 2616)在这里提到,服务器可以对主体应用转换。甚至有一个机制,使客户端能够知道其内存中的表示体是否保持当前状态,即:除非请求的表示保存时没有进行任何转换,否则服务器不得发送...ETag或Last-Modified。 - Slawomir Brzezinski

5
使用PUT创建资源的缺点是客户端必须提供表示其正在创建的对象的唯一ID。虽然客户端通常可以生成此唯一ID,但大多数应用程序设计者更喜欢他们的服务器(通常通过他们的数据库)创建此ID。在大多数情况下,我们希望我们的服务器控制资源ID的生成。那么我们该怎么办呢?我们可以改用POST而不是PUT。
所以:
Put = UPDATE
Post = INSERT
希望这对于您的特定情况有所帮助。

好的,我在谈论一个更新,我知道这个ID...我不是在谈论插入... - opensas
这无疑是为将 PUT 仅保留用于更新的论点,但 rfc7231 正式允许使用,因此我认为该决定应根据每个 API 的情况作出。 - Shadoninja

1

HTTP方法POST和PUT并不是CRUD的创建和更新的HTTP等效方法。它们都有不同的用途。在某些情况下,使用PUT创建资源或使用POST更新资源是完全可行、有效甚至更好的选择。

当您可以通过特定资源完全更新资源时,请使用PUT。例如,如果您知道一篇文章位于http://example.org/article/1234,您可以通过对此URL进行PUT来直接放置一篇新的资源表示形式的文章。

如果您不知道实际资源位置,例如,当您添加新文章但不知道要存储在哪里时,您可以将其POST到一个URL,并让服务器决定实际的URL。


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