用于从子资源列表中更新/添加/删除项目的 REST 设计

Posted

技术标签:

【中文标题】用于从子资源列表中更新/添加/删除项目的 REST 设计【英文标题】:REST design for update/add/delete item from a list of subresources 【发布时间】:2017-07-19 08:33:35 【问题描述】:

我想知道当您拥有包含子资源列表的资源时,最佳做法是什么。例如,您有资源作者,其中包含姓名、身份证、生日和书籍列表等信息。此书单仅与作者有关​​。所以,你有以下场景:

    您想将一本新书添加到图书列表中 您想更新列表中的一本书的名称 您想从列表中删除一本书

解决方案 1

我搜索了正确的设计,并找到了多种方法。我想知道是否有一种标准的设计方法。我认为书上的设计说有以下方法:

    添加:POST /authors/authorId/book/ 更新:PUT /authors/authorId/book/bookId 删除:DELETE /authors/authorId/book/bookId

解决方案 2

我的解决方案是只使用一个 PUT 方法来完成所有这 3 件事,因为书籍列表仅存在于对象作者内部,而您实际上是在更新作者。比如:

PUT /authors/authorId/updateBookList(并在作者对象中发送整个更新的书单)

我在我的场景中发现了多个错误。例如,从客户端发送更多数据,在客户端有一些逻辑,在 API 上进行更多验证,并且还依赖于客户端拥有最新版本的 Book List。

我的问题是:这样做是反模式吗?

情况 1. 在我的情况下,我的 API 正在使用另一个 API,而不是数据库。使用的 API 只有一种“updateBookList”方法,所以我猜想在我的 API 中复制这种行为也更容易。也对吗?

情况 2。但是,假设我的 API 将使用数据库,是否更适合使用解决方案 1?

另外,如果您能提供一些文章、书籍,您可以在其中找到类似的信息。我知道这种设计不是一成不变的,但一些指导方针会有所帮助。 (示例:来自 Book REST API Design Rulebook - Masse - O'Reilly)

【问题讨论】:

如果这是一个真正的问题而不是家庭作业,请记住书籍可以有多个作者。一本书应该是真正的***资源。 这是一个真正的问题,但在我的情况下,书籍列表只属于一位作者。例如,如果您删除某个作者,这些书籍将不再存在。 现在。如果业务需求发生变化,您将面临 API 更改中断的风险。当然,只有您知道这种可能性有多大。 【参考方案1】:

解决方案 2 听起来很像老式 RPC,其中调用了执行某些处理的方法。这就像 REST 反模式,因为 REST 的重点是资源而不是方法。您可以对资源执行的操作由底层协议(在您的情况下为 HTTP)给出,因此 REST 应遵守底层协议的语义(其为数不多的约束之一)。

此外,REST 并不关心您如何设置 URI,因此实际上没有 RESTful URL。对于自动化系统,遵循特定结构的 URI 与充当 URI 的随机生成的字符串具有相同的语义。尽管应用程序应该使用 rel 属性,但我们人类将意义放入字符串中,该属性为 URI 提供了应用程序可以使用的某种逻辑名称。期望 URL 具有特定逻辑组合的应用程序已经与 API 紧密耦合,因此违反了 REST 试图解决的原则,即客户端与服务器 API 的解耦。

如果您想以 RESTful 方式通过 PUT 更新(子)资源,则必须遵循 put 的语义,该语义基本上表明接收到的有效负载将替换更新前在给定 URI 上可访问的有效负载。

PUT 方法请求目标资源的状态 创建替换由表示定义的状态 包含在请求消息负载中。

...

POST 请求中的目标资源旨在处理 根据资源自身的语义封闭表示, 而 PUT 请求中的封闭表示定义为 替换目标资源的状态。因此,PUT 的意图 是幂等的并且对中间人可见,即使确切的 效果只有源服务器知道。

关于部分更新,RFC 7231 指出,部分更新可以通过使用 @Alexandru 建议的 PATCH 或直接在有效负载替换内容的子资源处发出 PUT 请求子资源与有效载荷中的一个。对于包含子资源的资源,这具有部分更新的影响。

部分内容更新可以通过 以状态重叠的单独标识资源为目标 较大资源的一部分,或通过使用不同的方法 已专门为部分更新定义(例如, PATCH 方法在 [RFC5789] 中定义)。

因此,在您的情况下,您可以通过PUT 操作将更新的图书收藏直接发送到类似.../author/authorId/books 的资源,以替换旧收藏。因为这对于写过许多出版物的作者来说可能无法很好地扩展PATCH 可能更可取。但是请注意,PATCH 需要原子和事务行为。要么所有动作都成功,要么都不成功。如果在操作过程中发生错误,您必须将所有已执行的步骤恢复原状。

关于您对进一步文献的要求,SO 不是问这个问题的正确地方,因为这有一个自己的离题关闭/标记原因。

【讨论】:

【参考方案2】:

我会选择第一个选项并使用单独的方法,而不是将所有逻辑都塞进一个通用的PUT 中。即使您依赖 API 而不是数据库,这也只是一个第三方依赖项,您应该可以随时切换,而无需重构太多代码。

话虽如此,如果您要允许同时更新大量书籍,那么PATCH 可能是您的朋友:

查看RFC 6902(定义补丁标准),从客户端的角度来看,API 可以这样调用

PATCH /authors/authorId/book
[
     "op": "add", "path": "/ids", "value": [ "24", "27", "35" ],
     "op": "remove", "path": "/ids", "value": [ "20", "30" ]
]

【讨论】:

您应该查看 HTTP PATCH (tools.ietf.org/html/rfc5789) 语义而不是 JSON PATCH。【参考方案3】:

从技术上讲,解决方案 1 很简单。

REST API URL 由资源(以及标识符和过滤器属性名称/值)组成。它不应包含动作(动词)。使用动词会鼓励创建愚蠢的 API。

例如我知道一个现实生活中的 API 希望你这样做

/getrecords 上执行POST获取所有记录 在/putrecords 上执行POST添加新记录

选择解决方案 2 的原因与技术无关。

对于需求 #2(您想从列表中更新一本书的名称),可以使用 JSON PATCH 语义,但使用 HTTP PATCH (https://tools.ietf.org/html/rfc5789) 语义来设计 URL(不是 JSON PATCH 语义as suggested Alexandru Marculescu)。

/authors/authorId/book/bookId 上执行PATCH,其中正文仅包含PK 和更改的属性。而不是: 要更新:PUT /authors/authorId/book/bookId

JSON PATCH 语义当然可以用于设计 PATCH 请求的主体,但这只会使 IMO 变得复杂。

【讨论】:

以上是关于用于从子资源列表中更新/添加/删除项目的 REST 设计的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 REST 从子文档数组中发布、修补和删除项目

如何使用 REST 从子文档数组中发布、修补和删除项目

从子查询中获取表名以在一行中用于 Oracle 中的删除/更新/插入子句

REST风格的请求格式

从子列表中删除对象已删除的android

什么是RESTful