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

Wes*_*Wes 270 rest api-design

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


更新:请参阅下面的答案,看看我做了什么.

小智 157

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

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

  2. 嵌套端点通常需要冗余端点.换句话说,您通常需要额外的/员工端点,以便获得跨部门的员工列表.如果您有/员工,/公司/部门/员工到底会给您带来什么?

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

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

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

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

  • 看到这个答案非常令人耳目一新.在被教导为"正确的方法"之后,我已经使用嵌套端点几个月了.我得出了你上面列出的所有相同的结论.使用非嵌套设计更容易. (21认同)
  • 您似乎将一些缺点列为优点。“只需将更多参数塞入单个端点”使 API 更难记录和学习,而不是相反。;-) (5认同)
  • 不是这个答案的粉丝.由于您添加了嵌套资源,因此无需引入冗余端点.如果父母真正拥有嵌套资源,那么拥有多个父母返回的相同资源也不是问题.获取父资源以了解如何与嵌套资源交互不是问题.一个好的可发现的REST API应该这样做. (4认同)
  • 如果最终资源都具有唯一的 ID,则不需要中间父 ID。例如,要通过 id 获取员工,您可以使用 GET /companies/departments/employees/{empId} ,或者要获取公司 123 中的所有员工,您可以使用 GET /companies/123/departments/employees/ 保持路径分层,使其更加明显我认为,您可以访问中间资源来过滤/创建/修改,并有助于提高可发现性。 (4认同)
  • @Scottm - 我遇到的嵌套资源的一个缺点是,如果父资源ID不正确/不匹配,它可能导致返回不正确的数据.假设没有授权问题,则由api实现来验证嵌套资源确实是传递的父资源的子代.如果未对此检查进行编码,则api响应可能不正确,从而导致损坏.你的想法是什么? (2认同)
  • 第 2 点和第 5 点主要基于意见。您声称查询参数比端点对应点更容易合并到现有 API 中、更容易记录、对消费者更直接,这似乎完全没有根据。我认为添加一个单独的端点来处理链接到父端点的查询实体比将该业务逻辑合并到已经繁忙的控制器中更容易开发。我还发现,在 URI 路径中表达资源关系非常适合将它们隐藏在查询参数中,例如:`?page=X&department_id=Y&sort=Z&...` (2认同)

jer*_*myh 137

你所做的是正确的.通常,同一资源可能有许多URI - 没有规则表明您不应该这样做.

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

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

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

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

company/{companyid}/employees

哪个会让该公司的员工回归.这取决于您的消费客户需要什么 - 这就是您应该设计的内容.

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

  • @abookyun如果你需要两个路由,那么它们之间的重复控制器代码可以被抽象为服务对象. (13认同)
  • 这指出了RESTful的精神,如果只考虑一个有意义的**资源**,没有规则说你应该或不应该这样做.但更进一步,我想知道在这种情况下**不重复代码**的最佳做法是什么. (11认同)
  • 虽然没有禁止,但我认为只有一条通往资源的路径更优雅——让所有的心智模型更简单。如果有任何嵌套,我也更喜欢 URI 不更改它们的资源类型。例如`/company/*` 应该只返回公司资源,而根本不改变资源类型。这些都不是由 REST 指定的——它通常是一个不明确的指定——只是个人偏好。 (2认同)

Wes*_*Wes 73

我把我从问题所做的事情转移到了更多人可能会看到它的答案.

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

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

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

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

  • 我也提出了相同类型的设计.我认为创建像"他们属于的地方"这样的东西是直观的,但是仍然能够在全球范围内列出它们.当资源必须有父母的关系时更是如此.然后全局创建该资源并没有那么明显,但在像这样的子资源中进行这样做是完全合理的. (4认同)

Lon*_*yen 26

我已经阅读了上述所有答案,但似乎没有共同的策略.我在Microsoft Documents中找到了一篇关于Design API最佳实践的好文章.我想你应该参考.

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

.

小费

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

  • 您提供的参考令人惊奇,并且您不制作复杂的URI也很突出。 (2认同)

red*_*ben 10

您的网址外观与REST无关.什么都可以.它实际上是一个"实现细节".就像你如何命名变量一样.它们必须具有独特性和耐用性.

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

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

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

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

  • 您的媒体类型有一个链接属性,列出可用/相关链接.
  • 每个链接都由一个关系标识(就像浏览器知道链接[rel ="stylesheet"]表示它的样式表或rel = favico是指向favicon的链接...)
  • 并且它知道这些关系意味着什么("公司"意味着公司列表,"搜索"意味着在资源列表上进行搜索的模板化网址,"部门"意味着当前资源的部门)

下面是一个示例HTTP交换(实体是yaml,因为它更容易编写):

请求

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

请求:链接到公司(使用之前的回复body.links.companies)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml
Run Code Online (Sandbox Code Playgroud)

响应:部分公司列表(在项目下),资源包含相关链接,如获取下一对公司的链接(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
Run Code Online (Sandbox Code Playgroud)

因此,当您看到链接/关系方式如何构建URL的路径部分时,您的API客户端没有任何价值.如果您正在将您的URL结构作为文档传达给您的客户,那么您不会根据" Richardson的成熟度模型 " 进行REST(或至少不是3级)

  • "作为API开发人员,您对API的好处/可理解性只是您的兴趣,而不是API客户端,代码中的变量名称也是如此." 为什么这不会有趣?这非常重要,除非您自己也使用API​​.这是用户体验的一部分,因此我认为对API客户端开发人员来说这很容易理解非常重要.通过清楚地链接资源使事情更容易理解当然是一个奖励(你提供的网址中的第3级).一切都应该是直观和合乎逻辑的,关系清晰. (7认同)
  • 我反对创建一个对人类不友好的API或UI.3级与否,我同意链接资源是一个好主意.但建议这样做"使改变URL方案成为可能"就是要脱离现实,以及人们如何使用API​​.所以这是一个糟糕的建议.但是确保在最好的世界中每个人都会处于3级REST.我合并了超链接并使用了一个人性化的可理解的URL方案.3级不排除前者,我认为应该关注.虽然好文章:) (4认同)

Max*_*val 9

我不同意这种道路

GET /companies/{companyId}/departments
Run Code Online (Sandbox Code Playgroud)

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

GET /departments?companyId=123
Run Code Online (Sandbox Code Playgroud)

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

GET /departments?companyId=123
Run Code Online (Sandbox Code Playgroud)

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

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.
Run Code Online (Sandbox Code Playgroud)

如果你想创建一个部门,那么

POST /departments
Run Code Online (Sandbox Code Playgroud)

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

  • 对我来说,只有当嵌套对象作为原子对象有意义时,这是一种可接受的方法。如果不是,将它们分开就没有任何意义。 (2认同)
  • 在获取公司时允许通过延迟加载来包含部门也是有意义的,例如`GET/companies/{companyId}?include = departments`,因为这样可以在单个HTTP中获取公司及其部门请求.Fractal做得非常好. (2认同)
  • 当您设置 ACL 时,您可能希望将“/departments”端点限制为只能由管理员访问,并且让每个公司只能通过“/companies/{companyId}/departments”访问自己的部门 (2认同)