REST嵌套资源的最佳做法是什么?


301

据我所知,每个单独的资源应该只有一条规范路径。因此,在以下示例中,好的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模式的最佳实践是什么,其中资源由其他资源拥有,但应该可单独查询?


1
尽管答案可能是相关的,但这几乎是与stackoverflow.com/questions/7104578/…中描述的问题相反的问题。这两个问题都与所有权有关,但是该示例暗示顶级对象不是拥有对象。
2014年

1
正是我想知道的。对于给定的用例,您的解决方案看起来不错,但是如果关系是聚合而不是组合怎么办?仍在努力找出此处的最佳实践...而且,此解决方案是否仅暗示关系的创建,例如雇用了现有人员,还是创建了人员对象?
Jakob O. 2014年

在我的虚拟例子中,它创造了一个人。尽管模仿我的实际问题,但我使用这些领域术语的原因是一个可以理解的示例。您是否浏览过相关问题,可能会使您与亲密关系陷入僵局。
2014年

我将我的问题分为一个答案和一个问题。
2015年

Answers:


152

您所做的是正确的。通常,对于同一资源可以有很多URI-没有规则说您不应该这样做。

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

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

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

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

company/{companyid}/employees

这将使该公司的员工返回。这取决于您的客户的需求-这就是您应该设计的。

但我希望所有URL处理程序都使用相同的后备代码来满足请求,以免您重复代码。


11
这表明了RESTful的精神,没有规则说只有首先考虑有意义的资源才应该或不应该这样做。但是,此外,我想知道在这种情况下不复制代码的最佳实践是什么。
2015年

13
@abookyun如果您需要两条路径,则可以将它们之间的重复控制器代码抽象为服务对象。
bgcode 2015年

这与REST无关。REST不在乎如何构造URL的路径部分...它关心的只是有效的,希望持久的URI ...
redben

从这个答案出发,我认为动态分段都是唯一标识符的任何api都不需要处理多个动态分段()。如果api提供了获取每种资源的方法,则可以在客户端库中或作为重复使用代码的一次性端点来进行每个请求。/company/3/department/2/employees/1
最大

1
尽管没有禁止,但我认为只有一种资源途径更为优雅-简化所有思维模型。我也更喜欢如果有任何嵌套,URI不会更改其资源类型。例如,/company/*应该只返回公司资源,而根本不更改资源类型。REST并没有指定所有这些内容-通常对其定义不佳-只是个人喜好。
kashif

174

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

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

  2. 嵌套端点通常需要冗余端点。换句话说,您经常会需要附加的/ employees终结点,这样您就可以获得跨部门的员工列表。如果您有/员工,那么/ companies /部门/员工会怎样购买您?

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

  4. 有时,资源可能具有多种类型的父母。结果是多个端点都返回相同的资源。

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

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


24
很高兴看到这个答案。在被告知这是“正确的方法”之后,我已经使用嵌套端点了几个月了。我得出了您上面列出的所有相同结论。非嵌套设计非常容易。
user3344977

6
您似乎列出了一些不利因素。“仅将更多参数填充到单个端点中”会使API更加难以记录和学习,而不是相反。;-)
Drenmi '17

4
不喜欢这个答案。不必仅仅因为添加了嵌套资源就引入冗余端点。如果多个父级真正拥有嵌套资源,那么由多个父级返回相同的资源也不是问题。获取父资源来学习如何与嵌套资源进行交互不是问题。一个好的可发现的REST API应该做到这一点。
Scottm '17

3
@Scottm-我遇到的嵌套资源的一个缺点是,如果父资源ID不正确/不匹配,则可能导致返回不正确的数据。假设没有授权问题,则由api实现负责验证嵌套资源确实是传递的父资源的子代。如果未对此检查进行编码,则api响应可能不正确,从而导致损坏。你怎么看?
安迪·杜弗雷斯

1
如果最终资源都有唯一的ID,则不需要中间的父ID。例如,要按ID获取员工,您可以获取GET / companies / departments / employees / {empId},或者要获取公司123中的所有员工,您可以获取GET / companies / 123 / departments / employees /保持路径分层可以更清楚地了解在我看来,您可以使用中间资源进行过滤/创建/修改,并有助于发现。
PaulG

77

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

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

因此,在此示例中(仅列出更改资源的端点)

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

因此,每个集合都有根级别的资源。但是,创建所有者对象中。


4
我也提出了相同类型的设计。我认为创建这样的“它们所属的地方”很直观,但是仍然能够在全局范围内列出它们。当存在某种关系时,资源必须具有父级,则更是如此。然后,在全局范围内创建该资源并不会很明显,但是在这样的子资源中进行创建是很有意义的。
阿金(Joakim)

我猜您使用了POST意义PUT,否则。
Gerardo Lima

其实没有。请注意,我没有使用预先分配的ID进行创建,因为在这种情况下,服务器负责返回ID(在链接中)。因此,编写POST是正确的(无法在相同的实现上完成)。但是,看跌期权会更改整个资源,但仍可在同一位置使用,因此我将其放置。PUT与POST是另一回事,也存在争议。例如stackoverflow.com/questions/630453/put-vs-post-in-rest
Wes

@Wes甚至我更喜欢将动词方法修改为父级。但是,您看到很好接受全局资源的传递查询参数吗?例如:查询参数为company = company-id的POST /部门
Ayyappa '18

1
@Mohamad如果您认为在理解和应用约束方面都更容易采用其他方法,请随时给出答案。关于在这种情况下使映射显式。它可以使用参数,但实际上就是问题所在。什么是最好的方法。
Wes

35

我已经阅读了以上所有答案,但似乎他们没有共同的策略。我从Microsoft Documents找到了一篇有关Design API最佳做法的好文章。我想你应该参考。

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

小费

避免要求资源URI比复杂 collection/item/collection


3
您提供的参考令人惊奇,而且您不制作复杂的URI也很突出。
vicco

因此,当我要为用户创建团队时,应该是POST / teams(主体中的userId)还是POST / users /:id / teams
coinhndp

@coinhndp嗨,您应该使用POST / teams,并且在授权访问令牌后可以获取userId。我的意思是,当您创建资料时,您需要授权码,对吗?我不知道您使用的是什么框架,但是我确定您可以在API控制器中获取userId。例如:在ASP.NET API中,从ApiController的方法内调用RequestContext.Principal。在Spring Secirity中,SecurityContextHolder.getContext()。getAuthentication()。getPrincipal()将为您提供帮助。在AWS NodeJS Lambda中,标题对象中的名称为cognito:username。
阮长

那么POST / users /:id / teams有什么问题。我认为建议您在上面发布的Microsoft文档中使用
coinhndp

@coinhndp如果您以管理员身份创建团队,那很好。但是,作为普通用户,我不知道为什么在路径中需要userId?我想我们有user_A和user_B,如果user_A呼叫POST / users / user_B / teams,如果user_A可以为user_B创建新团队,您怎么看?因此,在这种情况下无需传递userId,可以在授权后获取userId。但是,例如,team /:id / projects可以很好地在团队和项目之间建立关系。
阮长

10

URL的外观与REST无关。什么都可以。它实际上是一个“实现细节”。因此就像您如何命名变量一样。它们所必须的是独特且耐用的。

不要为此浪费太多时间,只需做出选择并坚持下去/保持一致即可。例如,如果您使用层次结构,则将对所有资源进行处理。如果使用查询参数...等,就像代码中的命名约定一样。

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

REST API中URL的美观程度/可理解性只是作为API开发人员(而不是API客户端)引起您的兴趣,而不像您代码中的变量名一样。

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

  • 您的媒体类型具有links属性,其中列出了可用/相关的链接。
  • 每个链接都由一种关系标识(就像浏览器知道link [rel =“ stylesheet”]表示其样式表一样,或者rel = favico是指向收藏夹的链接...)
  • 并知道这些关系的含义(“公司”表示公司列表,“搜索”表示用于在资源列表上进行搜索的模板化网址,“部门”表示当前资源的部门)

下面是一个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成熟度模型 ”的Level 3 )。


7
“作为API开发人员,您对作为REST API的URL的友好程度/理解程度仅是您感兴趣,而不是API客户端,就像您代码中的变量名称一样。” 为什么这不会很有趣?如果您自己以外的任何人也正在使用API​​,则这非常重要。这是用户体验的一部分,因此对于API客户端开发人员来说,易于理解是非常重要的。通过清楚地链接资源,使事情变得更容易理解,这当然是一种奖励(您提供的URL中的第3级)。一切都应该是直观且合乎逻辑的,并且关系明确。
约阿希姆(Joakim)

1
@Joakim如果您要创建3级rest API(作为应用程序状态引擎的超文本),则客户端绝对不关心url的路径结构(只要它是有效的)。如果您的目标不是第3级,那么是的,这很重要,应该可以猜测。但是真正的REST是3级。一篇不错的文章:martinfowler.com/articles/richardsonMaturityModel.html
redben

4
我反对创建对人类不友好的API或UI。是否达到3级,我同意链接资源是一个好主意。但是建议这样做“使更改URL方案成为可能”是与现实以及人们如何使用API​​脱节的。因此,这是一个错误的建议。但是,可以肯定的是,所有人都将处于3级REST。我合并了超链接,并使用了易于理解的URL方案。第3级不排除前者,我认为应该关注其中的一个。好文章虽然:)
Joakim

当然应该出于维护性和其他方面的考虑而关心,我认为您错过了我的回答的重点:URL的外观不值得很多思考,您应该“做出选择并坚持下去/坚持下去”。一致”,就像我在回答中所说的。对于REST API,至少在我看来,用户友好性不在URL中,它主要是在(媒体类型)中,无论如何我希望您能理解我的观点:)
redben

9

我不同意这种路径

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(如果部门只能链接到一个公司)。


1
对我来说,仅当嵌套对象作为原子对象有意义时,这才是可接受的方法。如果它们不是,将它们分开真的没有任何意义。
Simme,2016年

这就是我所说的,如果您还希望能够检索部门,即是否要使用/ departments端点。
Maxime Laval

2
例如GET /companies/{companyId}?include=departments,在获取公司时允许通过延迟加载将部门包括在内也是有意义的,例如,因为这允许在单个HTTP请求中同时获取公司及其部门。分形确实做得很好。
马修·戴利

1
设置ACL时,您可能希望限制/departments端点只能由管理员访问,并且让每个公司只能通过`/ companies / {
companyId

@MatthewDaly OData也使用$ expand做到了这一点
Rob Grant

1

Rails为此提供了一种解决方案:浅层嵌套

我认为这是一个好习惯,因为当您直接处理已知资源时,就无需使用嵌套路由,如此处其他答案所述。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.