RESTful 恢复已删除资源

71

支持撤销删除或延迟/批量删除对于数据服务来说是一个相当常见的需求。我想知道的是如何以RESTful的方式实现这一点。我在几种不同的选项之间犹豫(其中没有一种对我来说特别有吸引力)。我认为这些不同选项中的共同点是需要一个API,该API返回特定资源类型标记为已删除的所有资源。

以下是我考虑过的一些选项及其优缺点:

将资源标记为已删除的选项:

  • 使用HTTP DELETE将资源标记为已删除。
  • 使用HTTP PUT / POST更新已删除标志。这不太对,因为它将本质上是删除的内容从HTTP DELETE方法映射到其他HTTP方法中。

获取已标记为删除的资源时的选项:

  • 为标记为已删除的资源返回HTTP状态404。干净透明,但我们如何区分真正删除的资源和仅标记为删除的资源。
  • 返回HTTP状态410。提供了区分它们的方法,但410技术上表示“预计应被视为永久性。具有链接编辑功能的客户端应在用户批准后删除对请求URI的引用。”这里的“预计”和“应”这两个词可能有足够的空间。不确定客户端中有多少支持/了解410。
  • 返回HTTP状态200并包括指示资源已删除的标志字段。这似乎很奇怪,因为首先要删除它的想法是因为您实际上希望它不出现。这将责任推给客户端过滤已删除的资源。

包含此已删除资源的响应选项:

  • 省略标记为已删除的资源。清洁简单。但是如果您确实想知道有关已删除资源的信息呢?
  • 将它们与指示它们已被删除的字段一起包含在内。这将责任推向客户端过滤已删除的资源。如果您只想分页浏览活动或已删除的资源,则分页变得棘手。

更新已标记为删除的资源时的选项:

  • 使用HTTP状态码404。该资源已经消失,对吧?但是,你如何区分一个被标记为删除的资源和一个实际上被删除的资源之间的区别。 404响应中的HTTP正文可能有助于消除歧义,但是客户端需要解析/解释正文以消除歧义。也许响应头可以在这里提供帮助?哪一个?自定义标头?
  • 使用HTTP状态码409,并说明资源必须首先取消删除。
  • 取消删除已标记删除资源的选项:

    • 使用HTTP PUT / POST进行资源的更新操作并将其标记为活动状态。只要您不返回HTTP 404来获取资源的GET操作,此方法才有效,因为将PUT / POST用于“未找到”(404)的资源是没有意义的。
    • 使用HTTP PUT / POST进行资源的创建操作。问题在于哪些数据优先?在创建操作中发送的数据?还是正在取消删除的数据?从任何其他会返回它的查询中过滤掉它,然后,如果资源标识符指向标记为已删除的资源,则将创建资源的HTTP PUT / POST视为取消删除。
    • 专门用于取消删除已标记删除资源的REST路径。

    这绝非详尽无遗的列表。我只想罗列一些在我的脑海中反复出现的选项。

    我知道如何做这件事的答案通常是“取决于情况”。我感兴趣的是,您将使用什么资格/要求来做出决定?您如何看待它的实施或自己实施了它?


4
这绝对是我在StackOverflow上看过的最为充分准备和深思熟虑的问题之一。点个赞。 - Tyler Crompton
7个回答

11

按照规范来说:RFC 2616-9.7

  The DELETE method requests that the origin server delete the resource 
  identified by the Request-URI. This method MAY be overridden by human 
  intervention (or other means) on the origin server. The client cannot
  be guaranteed that the operation has been carried out, even if the 
  status code returned from the origin server indicates that the action
  has  been completed successfully. However, the server SHOULD NOT 
  indicate success unless, at the time the response is given, if it intends
  to delete the resource or move it to an inaccessible location.
当你删除一个资源时,服务器应该在它的一侧将该资源标记为已删除。实际上,它并不一定要删除该资源,只是不能保证操作已经完成。即使如此,服务器也不应该在资源没有被删除时说它已经被删除了。
  A successful response SHOULD be 200 (OK) if the response includes an entity
  describing the status, 202 (Accepted) if the action has not yet been enacted,
  or 204 (No Content) if the action has been enacted but the response does not
  include an entity.

如果操作被延迟,发送一个状态码为202的响应以及一个描述操作结果的实体主体。(想象一下代表服务器延迟删除资源的可轮询“任务”;它理论上可以永远保留在那个状态。)它只需要防止客户端再以原始形式检索它。使用状态码410作为响应代码,当“任务”完成或服务器删除资源时,返回状态码404。

但是,如果对于所涉及的资源,DELETE 的语义不合适,也许你不是想要删除,而是想要进行状态转换以改变资源状态但仍然保持其可访问性?在这种情况下,请使用 PUT/PATCH 更新资源并完成操作。


7
有没有关于如何在资源被删除后以RESTful方式恢复资源的建议? - Caleb
在这种情况下,您实际上并没有删除资源,而是限制了对其的访问。理论上,如果您已经DELETE了资源,那么就没有任何阻止您从该任务添加状态转换以“重新创建”它。从语义纯度的角度来看,我不太喜欢这种方法,但至少比绕过HTTP语义更干净。 - jmkeyes
回答不错,但由于2616已被7231取代,后者对DELETE语义有不同的描述,而且我还有其他要补充的内容,所以我发了一个新的回答。 - John B

9

简短版

你不能通过任何方法在原始URI上RESTfully恢复已删除的资源 - 这是不合逻辑的,因为对已删除的资源尝试进行的任何操作都应返回404或410。虽然规范中没有明确说明,但在DELETE方法的定义中1(加粗部分)中强烈暗示了这一点:

实际上,此方法类似于UNIX中的rm命令:它表示对源服务器的URI映射进行删除操作,而不是期望之前关联的信息被删除。

换句话说,当您DELETE了一个资源时,服务器不再将该URI映射到该数据。因此,您无法对其进行PUT或POST以进行更新,例如“将其标记为未删除”等(请记住,资源被定义为URI和某些基础数据之间的映射)。

一些解决方案

由于明确指出底层数据不一定被删除,因此不排除服务器作为DELETE实现的一部分创建新的URI映射,从而有效地创建可以稍后恢复的备份副本。

您可以有一个包含所有删除项目的“/deleted/”集合 - 但是您如何实际执行undelete操作呢?也许最简单的RESTful方法是让客户端使用GET检索项目,然后将其POST到所需的URL。

如果您需要能够将已删除的项目恢复到其原始位置怎么办?如果您使用支持它的媒体类型,则可以在从/deleted/集合获取的响应中包含原始URI。然后,客户端可以使用它进行POST。这样的响应在JSON中可能看起来像这样:

{
    "original-url":"/some/place/this/was/deleted/from",
    "body":<base64 encoded body>
}

客户端可以将该主体POST到该URI以执行未删除操作。
或者,如果您的资源定义允许移动的概念(通过更新“位置”属性或类似内容),则可以进行部分更新并避免整个对象的往返。或者,做大多数人所做的,实现类似RPC的操作,告诉服务器移动资源!非RESTful,但在大多数情况下可能效果很好。
如何决定这些事情:
关于如何决定这些事情的问题:您必须考虑删除在应用上下文中的含义以及为什么要删除它。在许多应用程序中,没有任何东西被删除,“删除”实际上只意味着“从所有进一步的查询/列表等中排除此项,除非我明确地取消删除”。因此,它实际上只是元数据或移动操作。在这种情况下,为什么要费心使用HTTP DELETE?一个原因可能是,如果您想要两层删除-软删除或临时版本是可撤销的,而硬/永久版本是...不可行的。
在没有特定应用程序上下文的情况下,我倾向于像这样实现它们:
我不想再看到这个资源,为了方便起见:POST部分更新以标记资源为“暂时删除” 我不想让任何人再能访问这个资源,因为它尴尬/归罪/花费我金钱等: HTTP DELETE
下一个要考虑的问题是:永久删除是否仅永久取消映射URI,以便再也没有人可以链接到它,还是必须清除底层数据?显然,如果您保留数据,则管理员甚至可以通过“永久”删除的资源进行恢复(但不能通过任何RESTful接口)。这样做的缺点是,如果数据所有者真的想要清除数据,则管理员必须在REST接口之外执行该操作。

如果一个资源在GET时返回404,从客户端的角度来看,它是被删除了还是从未存在过并没有区别。使用PUT重新创建资源与使用PUT首次创建资源相同,这没有任何问题。 - Evert
2
@Evert 你说得对。我在回答中提到不能将PUT或POST发送到已删除的URL,然后又提出了一个恰好做到这一点的解决方案!我的意思是说你不能PUT或POST来更新已删除的资源,换句话说,不能去掉“删除”标志或类似的内容,正如问题所建议的那样。我更新了我的答案以尝试反映这一点。 - John B

7
我认为最符合RESTful规范的解决方法是使用HTTP PUT来标记资源以进行删除(和取消删除),然后使用HTTP DELETE来永久删除该资源。要获取标记为删除的资源列表,我会在HTTP GET请求中使用一个参数,例如?state=markedForDeletion。 如果您未带有该参数请求已标记为删除的资源,则应返回“404 Not Found”状态。

7
“删除”(移到垃圾桶)的项目也可以被视为一种资源,对吗?那么我们可以通过以下方式之一访问此资源(例如,对于一个已删除的用户):
PATCH deleted_users/{id}
PATCH trash/users/{id}
PATCH deleted/users/{id}

也有一些人认为这是更加轻松的方式:

PATCH deleted/{id}?type=users

而在有效载荷中,内容如下:
{ deleted_at: null }

5

我也遇到了这个问题,已经在互联网上寻找最佳解决方案。由于我找不到任何正确的主要答案,因此这里是我的研究结果。

其他人正确地指出,DELETE 是正确的方法。您可以包括一个标志来确定它是立即永久 DELETE 还是移动到垃圾桶(可能只有管理员才能执行立即永久 DELETE)。

DELETE /api/1/book/33
DELETE /api/1/book/33?permanent

后端可以将书标记为已删除。假设您有一个SQL数据库,它可能是以下内容:
UPDATE books SET status = 'deleted' WHERE book_id = 33;

正如其他人所提到的,一旦执行了DELETE操作,集合中就不会返回该项。就SQL而言,这意味着您必须确保不返回状态为deleted的项。

SELECT * FROM books WHERE status <> 'deleted';

此外,当你进行 GET /api/1/book/33 时,必须返回404或410。 410的一个问题是它意味着永久消失(至少这是我对该错误代码的理解),所以只要该项目存在但被标记为“已删除”,就应该返回404,并在永久删除后返回410。
现在,正确的恢复方法是使用 PATCH。 与用于更新项的 PUT 相反,PATCH 预期是对项执行操作。从我所看到的内容来看,操作预期在有效载荷中。为了使其起作用,资源需要以某种方式可访问。正如其他人建议的那样,您可以提供一个 trashcan 区域,在删除后,书籍会出现在其中。以下是列出放进垃圾箱的书籍的示例:
GET /api/1/trashcan/books

[{"path":"/api/1/trashcan/books/33"}]

因此,现在生成的列表将包括书籍编号为33,然后您可以使用PATCH操作进行更新,例如:
PATCH /api/1/trashcan/books/33

{
    "operation": "undelete"
}

如果您想让操作更加通用,可以使用类似以下这样的东西:
PATCH /api/1/trashcan/books/33

{
    "operation": "move",
    "new-path": "/api/1/books/33"
}

然后,“移动”可以在您的界面中尽可能地用于其他URL更改。(我正在一个CMS上工作,其中页面的路径在名为tree的一个表中,每个页面都在另一个称为page的表中,并具有标识符。我可以通过将页面在我的tree表中移动到不同的路径来更改页面的路径!这就是PATCH非常有用的地方。)
不幸的是,RFC并没有清楚地定义PATCH,只是它应该与如上所示的操作一起使用,而不是像PUT那样接受表示目标项的新版本(可能是部分)的有效负载:
PUT /api/1/books/33

{
    "title": "New Title Here"
}

如果你同时支持的话,相应的PATCH请求将会是:

PATCH /api/1/books/33

{
    "operation": "replace",
    "field": "title",
    "value": "New Title Here"
}

我认为支持那么多个PATCH操作是不可行的。但是,我认为一些好的例子可以更好地说明为什么PATCH是正确的解决方案。

你可以这样想:使用补丁是为了更改一个虚拟字段或运行复杂的操作,例如移动,否则将需要GETPOSTDELETE(假设DELETE是立即执行的,你可能会遇到错误并最终只完成部分移动……)。在某种程度上,PATCH类似于拥有任意数量的方法。UNDELETEMOVE方法的工作方式类似,但RFC明确表示存在一组标准化方法,你应该坚持使用它们,PATCH给你足够的空间,不必添加自己的方法。虽然我没有看到规范中有关于不应添加自己的方法的内容。但如果你这样做,请确保清楚地记录它们。


1
我认为这是使用 PATCH 的充分理由。 - jiminikiz

0
我们已经强制模型创建一个

POST /modelname/:id/undelete


4
这不是符合RESTful标准的,因为路径中有一个动词:undelete。 - Ihor M.
2
我确认。我说过“我们已经强制模型”……没有UNDELETE HTTP动词,所以我们选择了这个妥协。 - realtebo
我不是在谈论http动词,而是在谈论你路径中的动词:“undelete”。 - Ihor M.

0

另一种略微违反REST原则的方法是,在调用资源的DELETE方法时执行软删除,然后在一定时间后执行永久删除。我实现了这种方法,并且对于只想为用户提供快速撤消操作的应用程序而言,它运行得非常好。

我的具体实现是,我只是添加了一个名为“deletionTimestamp”的附加列,如果删除了资源,则设置该列。然后,我有一个CRON作业每24小时检查资源的删除时间,如果删除时间太久远,则会永久删除该行。

对我来说,实际删除并保存项目副本的问题在于实现的大量开销。我有一些具有非常复杂的外键关系的资源,需要针对不同类型的已删除资源进行特定的实现。


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