ChatGPT解决这个技术问题 Extra ChatGPT

REST 嵌套资源的最佳实践是什么?

据我所知,每个单独的资源应该只有一个规范路径。那么在下面的示例中,好的 URL 模式是什么?

以公司的休息表示为例。在这个假设示例中,每个公司拥有 0 个或多个部门,每个部门拥有 0 个或多个员工。

没有关联公司,部门就无法存在。

没有相关部门,员工就无法存在。

现在我会发现资源模式的自然表示。

/companies 公司集合 - 接受新公司的 POST。获取整个系列。

/companies/{companyId} 单个公司。接受 GET、PUT 和 DELETE

/companies/{companyId}/departments 接受新项目的 POST。 (在公司内创建一个部门。)

/companies/{companyId}/departments/{departmentId}/

/companies/{companyId}/departments/{departmentId}/employees

/companies/{companyId}/departments/{departmentId}/employees/{empId}

鉴于限制,在每个部分中,我觉得如果嵌套有点深,这是有道理的。

但是,如果我想列出 (GET) 所有公司的所有员工,我的困难就来了。

该资源模式将最接近地映射到 /employees(所有员工的集合)

这是否意味着我也应该拥有 /employees/{empId},因为如果是这样,那么有两个 URI 可以获取相同的资源?

或者也许整个模式应该被展平,但这意味着员工是一个嵌套的顶级对象。

在基本级别 /employees/?company={companyId}&department={deptId} 返回与嵌套最深的模式完全相同的员工视图。

对于资源由其他资源拥有但应该可以单独查询的 URL 模式的最佳实践是什么?

这几乎与 stackoverflow.com/questions/7104578/… 中描述的问题完全相反,尽管答案可能是相关的。这两个问题都与所有权有关,但该示例暗示顶级对象不是拥有者。
正是我想知道的。对于给定的用例,您的解决方案似乎很好,但是如果关系是聚合而不是组合呢?仍在努力找出最佳实践是什么...此外,此解决方案是否仅意味着创建关系,例如雇用现有人员还是创建人员对象?
它在我的虚构示例中创建了一个人。我使用这些领域术语的原因是它是一个可以合理理解的示例,尽管它模仿了我的实际问题。您是否查看过可能会更阻碍您建立聚合关系的链接问题。
我把我的问题分成了一个答案和一个问题。

P
Patc

我尝试了两种设计策略 - 嵌套和非嵌套端点。我发现:

如果嵌套资源具有主键而您没有其父主键,则嵌套结构需要您获取它,即使系统实际上并不需要它。嵌套端点通常需要冗余端点。换句话说,您通常需要额外的 /employees 端点,以便您可以获取跨部门的员工列表。如果你有 /employees,那么 /companies/departments/employees 到底给你买了什么?嵌套端点没有发展得那么好。例如,您现在可能不需要搜索员工,但以后可能需要,如果您有嵌套结构,则别无选择,只能添加另一个端点。使用非嵌套设计,您只需添加更多参数,这更简单。有时一个资源可能有多种类型的父级。导致多个端点都返回相同的资源。冗余端点使文档更难编写,也使 api 更难学习。

简而言之,非嵌套设计似乎允许更灵活和更简单的端点模式。


看到这个答案非常令人耳目一新。在被教导这是“正确的方式”之后,我已经使用嵌套端点几个月了。我得出了与您上面列出的所有相同的结论。使用非嵌套设计更容易。
您似乎将一些缺点列为优点。 “只是将更多参数塞进一个端点”使得 API 更难记录和学习,而不是相反。 ;-)
不喜欢这个答案。没有必要仅仅因为添加了嵌套资源就引入了冗余端点。多个父级返回相同的资源也不是问题,只要这些父级真正拥有嵌套资源。让父资源学习如何与嵌套资源进行交互不是问题。一个好的可发现 REST API 应该可以做到这一点。
@Scottm - 我遇到的嵌套资源的一个缺点是,如果父资源 ID 不正确/不匹配,它可能会导致返回不正确的数据。假设没有授权问题,则由 api 实现来验证嵌套资源确实是传递的父资源的子资源。如果未对此检查进行编码,则 api 响应可能不正确,从而导致损坏。你觉得呢?你有没有什么想法?
如果最终资源都具有唯一 ID,则不需要中间父 ID。例如,要通过 id 获取员工,您有 GET /companies/departments/employees/{empId} 或获取公司 123 中的所有员工,您有 GET /companies/123/departments/employees/在我看来,您可以使用中间资源来过滤/创建/修改并帮助提高可发现性。
J
Joakim

你所做的是正确的。一般来说,同一个资源可以有多个 URI - 没有规则说你不应该这样做。

通常,您可能需要直接访问项目或作为其他项目的子集 - 所以您的结构对我来说很有意义。

仅仅因为员工可以在部门下访问:

company/{companyid}/department/{departmentid}/employees

并不意味着它们也不能在公司下访问:

company/{companyid}/employees

这将返回该公司的员工。这取决于您的消费客户需要什么——这就是您应该设计的目标。

但我希望所有 URL 处理程序都使用相同的支持代码来满足请求,这样您就不会重复代码。


这是在指出 RESTful 的精神,没有规则说你应该或不应该做,只要你首先考虑一个有意义的资源。但进一步,我想知道在这种情况下不复制代码的最佳做法是什么。
@abookyun 如果您需要两条路线,则可以将它们之间重复的控制器代码抽象为服务对象。
这与 REST 无关。 REST 不关心你如何构建 URL 的路径部分……它关心的只是有效的、希望是持久的 URI……
按照这个答案,我认为动态段是所有唯一标识符的任何 api 都不需要处理多个动态段 (/company/3/department/2/employees/1)。如果 api 提供了获取每个资源的方法,那么可以在客户端库中或作为重用代码的一次性端点来完成每个请求。
虽然没有禁止,但我认为只有一条通往资源的路径更优雅——让所有心智模型更简单。如果有任何嵌套,我也更喜欢 URI 不会更改其资源类型。例如 /company/* 应该只返回公司资源而根本不改变资源类型。 REST 没有指定这些 - 它通常指定得很差 - 只是个人喜好。
W
Wes

我已经将我所做的事情从问题转移到了更多人可能会看到的答案。

我所做的是在嵌套端点处创建端点,用于修改或查询项目的规范端点不在嵌套资源处。

所以在这个例子中(只列出改变资源的端点)

POST /companies/ 创建一个新公司返回一个链接到创建的公司。

POST /companies/{companyId}/departments 当一个部门被创建时,新部门返回一个链接到 /departments/{departmentId}

PUT /departments/{departmentId} 修改一个部门

POST /departments/{deparmentId}/employees 创建新员工 返回指向 /employees/{employeeId} 的链接

因此,每个集合都有根级资源。然而,创建是在拥有的对象中。


我也想出了相同类型的设计。我认为创建像“它们所属的地方”这样的东西是很直观的,但仍然能够在全球范围内列出它们。当资源必须有父级的关系时更是如此。然后在全局范围内创建该资源并没有那么明显,但是在这样的子资源中这样做是非常有意义的。
我猜您使用 POST 表示 PUT,否则。
实际上没有请注意,我没有使用预分配的 ID 进行创建,因为在这种情况下,服务器负责返回 ID(在链接中)。因此写 POST 是正确的(不能在相同的实现上做一个获取)。但是 put 更改了整个资源,但它仍然在同一位置可用,所以我 PUT 了。 PUT vs POST 是另一回事,也有争议。例如 stackoverflow.com/questions/630453/put-vs-post-in-rest
@Wes 即使我更喜欢将动词方法修改为在父级之下。但是,您是否看到为全局资源传递查询参数被很好地接受了?例如:POST /departments 带有查询参数 company=company-id
@Mohamad如果您认为另一种方式在理解和应用约束方面更容易,请随时给出答案。在这种情况下,它是关于使映射明确的。它可以与参数一起使用,但实际上这就是问题所在。什么是最好的方法。
L
Long Nguyen

我已经阅读了上述所有答案,但似乎他们没有共同的策略。我找到了一篇关于 best practices in Design API from Microsoft Documents 的好文章。我觉得你应该参考。

在更复杂的系统中,提供 URI 以使客户端能够在多个关系级别中导航可能很诱人,例如 /customers/1/orders/99/products。然而,如果资源之间的关系在未来发生变化,这种复杂程度可能难以维持并且不灵活。相反,尽量保持 URI 相对简单。一旦应用程序引用了资源,就应该可以使用该引用来查找与该资源相关的项目。可以将上述查询替换为 URI /customers/1/orders 以查找客户 1 的所有订单,然后 /orders/99/products 以查找此订单中的产品。

.

提示 避免要求比 collection/item/collection 更复杂的资源 URI。


您提供的参考以及您在不制作复杂 URI 方面的突出之处令人惊叹。
所以当我想为一个用户创建一个团队时,应该是 POST /teams (userId in thebody) 还是 POST /users/:id/teams
@coinhndp 嗨,您应该使用 POST /teams 并且您可以在授权访问令牌后获取 userId。我的意思是当你创建一个东西时你需要授权码,对吧?我不知道您使用的是什么框架,但我确信您可以在 API 控制器中获取 userId。例如:在 ASP.NET API 中,从 ApiController 的方法中调用 RequestContext.Principal。在 Spring Secirity 中,SecurityContextHolder.getContext().getAuthentication().getPrincipal() 将为您提供帮助。在 AWS NodeJS Lambda 中,即 cognito:username 在 headers 对象中。
那么 POST /users/:id/teams 出了什么问题。我认为您在上面发布的 Microsoft 文档中建议使用它
@coinhndp 如果您以管理员身份创建团队,那很好。但是,作为普通用户,我不知道您为什么需要 userId 在路径中?我想我们有 user_A 和 user_B,如果 user_A 调用 POST /users/user_B/teams,如果 user_A 可以为 user_B 创建一个新团队,你怎么看。所以,这种情况下不需要传递 userId,userId 可以在授权后获取。但是,例如,teams/:id/projects 可以很好地在团队和项目之间建立关系。
M
Maxime Laval

我不同意这种路径

GET /companies/{companyId}/departments

如果你想获得部门,我认为最好使用 /departments 资源

GET /departments?companyId=123

我想您有一个 companies 表和一个 departments 表,然后用您使用的编程语言进行类映射。我还假设部门可以附加到公司以外的其他实体,因此 /departments 资源很简单,将资源映射到表很方便,而且您不需要那么多端点,因为您可以重用

GET /departments?companyId=123

例如,对于任何类型的搜索

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.

如果你想创建一个部门,

POST /departments

应使用资源,请求正文应包含公司 ID(如果部门只能链接到一家公司)。


对我来说,只有当嵌套对象作为原子对象有意义时,这才是可接受的方法。如果它们不是,那么将它们分开是没有意义的。
这就是我所说的,如果您还希望能够检索部门,这意味着您是否将使用 /departments 端点。
在获取公司时允许通过延迟加载包含部门也可能有意义,例如 GET /companies/{companyId}?include=departments,因为这允许在单个 HTTP 请求中获取公司及其部门。 Fractal 在这方面做得非常好。
当您设置 acls 时,您可能希望将 /departments 端点限制为只能由管理员访问,并让每个公司只能通过 /companies/{companyId}/departments 访问自己的部门
@MatthewDaly OData 也可以很好地使用 $expand
r
redben

您的 URL 的外观与 REST 无关。什么都行。它实际上是一个“实施细节”。所以就像你如何命名你的变量一样。它们所要做的就是独一无二且经久耐用。

不要在这方面浪费太多时间,只需做出选择并坚持下去/保持一致。例如,如果您使用层次结构,那么您对所有资源都这样做。如果您使用查询参数...等,就像代码中的命名约定一样。

为什么这样 ?据我所知,“RESTful”API 是可浏览的(您知道……“超媒体作为应用程序状态的引擎”),因此 API 客户端并不关心您的 URL 是什么样的,只要它们是有效(没有搜索引擎优化,没有人需要阅读那些“友好的网址”,除了可能用于调试......)

REST API 中的 URL 有多好/可理解只对作为 API 开发人员的您感兴趣,而不是 API 客户端,就像代码中变量的名称一样。

最重要的是您的 API 客户端知道如何解释您的媒体类型。例如它知道:

您的媒体类型具有列出可用/相关链接的链接属性。

每个链接都由关系标识(就像浏览器知道 link[rel="stylesheet"] 表示它的样式表或 rel=favico 是指向 favicon 的链接......)

它知道这些关系的含义(“公司”表示公司列表,“搜索”表示用于对资源列表进行搜索的模板 url,“部门”表示当前资源的部门)

下面是一个 HTTP 交换示例(正文在 yaml 中,因为它更容易编写):

要求

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml

响应:主要资源的链接列表(公司、人员等...)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:04:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: this is your API's entrypoint (like a homepage)  
links:
  # could be some random path https://api.acme.local/modskmklmkdsml
  # the only thing the API client cares about is the key (or rel) "companies"
  companies: https://api.acme.local/companies
  people: https://api.acme.local/people

请求:链接到公司(使用先前响应的 body.links.companies)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml

响应:公司的部分列表(在项目下),资源包含相关链接,例如获取下几个公司的链接(body.links.next)另一个(模板化)搜索链接(body.links.search)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:06:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: representation of a list of companies
links:
  # link to the next page
  next: https://api.acme.local/companies?page=2
  # templated link for search
  search: https://api.acme.local/companies?query={query} 
# you could provide available actions related to this resource
actions:
  add:
    href: https://api.acme.local/companies
    method: POST
items:
  - name: company1
    links:
      self: https://api.acme.local/companies/8er13eo
      # and here is the link to departments
      # again the client only cares about the key department
      department: https://api.acme.local/companies/8er13eo/departments
  - name: company2
    links:
      self: https://api.acme.local/companies/9r13d4l
      # or could be in some other location ! 
      department: https://api2.acme.local/departments?company=8er13eo

因此,正如您所见,如果您采用链接/关系的方式构建 URL 的路径部分,对您的 API 客户端没有任何价值。如果您将 URL 的结构作为文档传达给您的客户,那么您没有在做 REST(或者至少不是按照“Richardson's maturity model”的第 3 级)


“一个 URL 在 REST API 中的好坏/可理解性只对作为 API 开发人员的你感兴趣,而不是 API 客户端,就像代码中变量的名称一样。”为什么这不有趣?如果除了您自己之外的任何人也在使用该 API,这非常重要。这是用户体验的一部分,所以我想说这对于 API 客户端开发人员来说易于理解是非常重要的。通过清楚地链接资源使事情变得更容易理解当然是一个奖励(您提供的网址中的第 3 级)。一切都应该是直观和逻辑清晰的关系。
@Joakim如果您正在制作3级rest API(超文本作为应用程序状态的引擎),那么客户端对url的路径结构绝对不感兴趣(只要它有效)。如果您的目标不是 3 级,那么是的,这很重要并且应该是可以猜测的。但真正的 REST 是 3 级。一篇好文章:martinfowler.com/articles/richardsonMaturityModel.html
我反对创建对人类不友好的 API 或 UI。 3 级与否,我同意链接资源是一个好主意。但建议这样做“使更改 URL 方案成为可能”是脱离现实,以及人们如何使用 API。所以这是一个不好的建议。但可以肯定的是,在最好的情况下,每个人都会处于 3 级 REST。我合并了超链接并使用了人类可以理解的 URL 方案。 3 级不排除前者,我认为应该关心。好文章虽然:)
出于可维护性和其他问题,当然应该关心,我认为您错过了我的回答的重点:网址的外观不值得深思,您应该“只是做出选择并坚持下去/成为一致”,正如我在答案中所说。在 REST API 的情况下,至少我认为,用户友好性不在 url 中,它主要在(媒体类型)无论如何我希望你理解我的观点 :)
p
partydrone

Rails 对此提供了解决方案:shallow nesting

我认为这很好,因为当您直接处理已知资源时,无需使用嵌套路由,正如此处其他答案中所讨论的那样。


您实际上应该提供博客中回答问题的部分并提供参考链接。
@reoxey,文本“浅嵌套”链接到解释浅嵌套的 Rails 文档。它不工作吗?
该链接的主要问题是它带您进入示例的中途,并且与语言无关...我不了解 Ruby,也不了解示例中的代码实际上在做什么,因此,除非我愿意充分研究冗长的文档,学习一些Ruby,然后学习一些Rails,这对我没有用。这个答案应该用伪代码/结构化英语总结文章/手册描述的技术,以更好地表达你在这里的建议。
u
user566245

根据 django rest 框架文档:

通常,我们建议尽可能使用平面样式的 API 表示,但嵌套 URL 样式在适度使用时也是合理的。

https://www.django-rest-framework.org/api-guide/relations/#example_2