【问题标题】:What are best practices for REST nested resources?REST 嵌套资源的最佳实践是什么?
【发布时间】:2014-01-23 22:37:25
【问题描述】:

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

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

一个部门不可能存在没有关联公司。

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

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

  • /companies 公司集合 - 接受新公司的认购。获取整个系列。
  • /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/… 中描述的问题完全相反,尽管答案可能是相关的。这两个问题都与所有权有关,但该示例暗示顶级对象不是拥有者。
  • 正是我想知道的。对于给定的用例,您的解决方案似乎很好,但是如果关系是聚合而不是组合呢?仍在努力弄清楚这里的最佳实践是什么......此外,这个解决方案是否仅意味着创建关系,例如现有人员受雇还是创建人员对象?
  • 它在我的虚构示例中创建了一个人。我使用这些领域术语的原因是它是一个可以合理理解的示例,尽管它模仿了我的实际问题。您是否查看过可能更阻碍您建立关系的链接问题。
  • 我已将我的问题分为一个答案和一个问题。

标签: rest api-design


【解决方案1】:

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

  1. 如果嵌套资源有主键而您没有其父主键,则嵌套结构需要您获取它,即使系统实际上并不需要它。

  2. 嵌套端点通常需要冗余端点。换句话说,您通常需要额外的 /employees 端点,以便您可以获取跨部门的员工列表。如果你有 /employees,/companies/departments/employees 到底给你买了什么?

  3. 嵌套端点的发展没有那么好。例如。您现在可能不需要搜索员工,但以后可能需要,如果您有嵌套结构,则别无选择,只能添加另一个端点。使用非嵌套设计,您只需添加更多参数,这更简单。

  4. 有时一个资源可能有多种类型的父级。导致多个端点都返回相同的资源。

  5. 冗余端点使文档更难编写,也使 api 更难学习。

简而言之,非嵌套设计似乎允许更灵活、更简单的端点架构。

【讨论】:

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

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

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

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

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

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

company/{companyid}/employees

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

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

【讨论】:

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

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

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

所以在本例中(仅列出更改资源的端点)

  • POST /companies/ 创建新公司返回创建公司的链接。
  • POST /companies/{companyId}/departments 当一个部门被创建时,新部门返回一个链接到/departments/{departmentId}
  • PUT/departments/{departmentId}修改部门
  • POST /departments/{deparmentId}/employees 创建新员工 返回/employees/{employeeId} 的链接

因此,每个集合都有根级资源。但是 createowning 对象中。

【讨论】:

  • 我也想出了相同类型的设计。我认为创建像“它们所属的地方”这样的东西是很直观的,但仍然能够在全球范围内列出它们。当资源必须有父级的关系时更是如此。然后在全局范围内创建该资源并没有那么明显,但是在这样的子资源中这样做是非常有意义的。
  • 我猜你使用了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 如果您认为另一种方式在理解和应用约束方面更容易,请随时给出答案。在这种情况下,它是关于使映射明确的。它可以与参数一起使用,但实际上这就是问题所在。什么是最好的方法。
【解决方案4】:

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

在更复杂的系统中,提供 URI 可能很诱人 使客户能够浏览多个层次的关系, 比如/customers/1/orders/99/products. 不过这个级别的 复杂性可能难以维护并且不灵活,如果 未来资源之间的关系会发生变化。 相反,尝试 保持 URI 相对简单。一旦应用程序引用了 资源,应该可以使用此引用来查找项目 与该资源有关。前面的查询可以替换为 URI /customers/1/orders 查找客户 1 的所有订单,以及 然后/orders/99/products按此顺序查找产品。

.

提示

避免要求资源 URI 比 collection/item/collection.

【讨论】:

  • 您提供的参考资料以及您在不制作复杂 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 in 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 可以在授权后获取。但是,team/:id/projects 可以很好地建立团队和项目之间的关系。
【解决方案5】:

我不同意这种方式

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} 访问自己的部门/部门`
  • @MatthewDaly OData 也可以很好地使用 $expand
【解决方案6】:

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

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

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

一个 URL 在 REST API 中的好坏/易懂程度只对作为 API 开发人员的您感兴趣,而不是 API 客户端,就像代码中的变量名称一样。

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

  • 您的媒体类型具有列出可用/相关链接的链接属性。
  • 每个链接都由关系标识(就像浏览器知道 link[rel="stylesheet"] 表示其样式表或 rel=favico 是指向网站图标的链接...)
  • 它知道这些关系的含义(“公司”表示公司列表,“搜索”表示用于搜索资源列表的模板 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)

【讨论】:

  • “只有作为 API 开发人员而不是 API 客户端的您才会对 REST API 中的 URL 有多好/可理解感兴趣,就像代码中的变量名称一样。”为什么这不有趣?如果除了您自己之外的任何人也在使用该 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 级不排除前者,我认为应该关心。好文章虽然:)
  • 当然应该关心可维护性和其他问题,我认为你错过了我的回答的重点:url 的外观不值得思考,你应该“只是让正如我在回答中所说,一个选择并坚持下去/保持一致”。在 REST API 的情况下,至少我认为,用户友好性不在 url 中,它主要在(媒体类型)无论如何我希望你理解我的观点 :)
【解决方案7】:

根据 django rest 框架文档:

一般来说,我们建议在可能的情况下为 API 表示使用扁平样式,但嵌套 URL 样式在适度使用时也是合理的。

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

【讨论】:

    【解决方案8】:

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

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

    【讨论】:

    • 您实际上应该提供博客中回答问题的部分并提供参考链接。
    • @reoxey,文本“浅层嵌套”链接到解释浅层嵌套的 Rails 文档。不行吗?
    • 该链接的主要问题是它带您进入示例的中途,并且与语言无关...我不了解 Ruby,也不了解其中的代码example实际上是在做的,因此,除非我愿意充分研究冗长的文档,学习一些Ruby,然后学习一些Rails,否则对我来说毫无用处。这个答案应该用伪代码/结构化英语总结文章/手册描述的技术,以更好地表达您在这里的建议。
    猜你喜欢
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-06
    • 1970-01-01
    • 1970-01-01
    • 2020-04-30
    • 1970-01-01
    相关资源
    最近更新 更多