题 REST API设计:嵌套集合与新根目录


这个问题是关于最佳REST API设计以及我在嵌套资源和根级别集合之间选择的问题。

为了演示这个概念,假设我有收藏 CityBusiness,和 Employees。典型的API可以如下构造。想象一下,ABC,X7N和WWW是关键,例如的GUID:

GET Api/City/ABC/Businesses                       (returns all Businesses in City ABC)
GET Api/City/ABC/Businesses/X7N                   (returns business X7N)
GET Api/City/ABC/Businesses/X7N/Employees         (returns all employees at business X7N)
PUT Api/City/ABC/Businesses/X7N/Employees/WWW     (updates employee WWW)

这显得很干净,因为它遵循原始域结构 - 业务在城市,员工在公司。个人物品可通过集合下的钥匙进入(例如 ../Businesses 返回所有业务,而 ../Businesses/X7N 返回个人业务)。

以下是API消费者需要做的事情:

  • 在城市中获得业务 (GET Api/City/ABC/Businesses)
  • 让所有员工都参与进来 (GET Api/City/ABC/Businesses/X7N/Employees)
  • 更新个人员工信息 (PUT Api/City/ABC/Businesses/X7N/Employees/WWW)

第二次和第三次调用虽然看起来在正确的位置,但使用了许多实际上不必要的参数。

  • 为了让员工参与到业务中,唯一需要的参数是业务的关键(X7N)。
  • 要更新单个员工,唯一的参数需要员工的密钥(WWW

后端代码中的任何内容都不需要非关键信息来查找业务或更新员工。因此,相反,以下端点看起来更好:

GET Api/City/ABC/Businesses                       (returns all Businesses in City ABC)
GET Api/Businesses/X7N                            (returns business X7N)
GET Api/Businesses/X7N/Employees                  (returns all employees at business X7N)
PUT Api/Employees/WWW                             (updates employee WWW)

如你所见,我创造了一个新的  对于企业和员工,即使从域的角度来看,它们也是子/子子集合。

这两种解决方案对我来说都不是很干净。

  • 第一个示例要求提供不必要的信息,但结构的方式对消费者来说是“自然的”(来自集合的单个项目通过较低的叶子检索)
  • 第二个例子只询问必要的信息,但不是以“自然”的方式构建 - 子集可以通过根访问
  • 添加新员工时,单个员工root将无法工作,因为我们需要知道要将员工添加到哪个业务,这意味着该调用至少必须位于Business根目录下,例如 POST Api/Businesses/X7N7/Employees,这让一切更加令人困惑。

有没有更清洁,第三种方式,我没想到?


23
2017-10-13 00:08


起源




答案:


我没有看到REST如何添加两个资源不能具有相同值的约束。该 resourceType/ID 这只是最简单的用例的一个例子,而不是从RESTful角度出发的最佳方式。

如果你读 第5.2.1.1段 罗伊菲尔丁的论文仔细考虑,你会注意到菲尔丁在a之间做了一个分歧  和a 资源。现在资源应该有一个唯一的URI,这是真的。但没有什么能阻止两个资源具有相同的价值:

例如,学术论文的“作者'首选版本”是其值随时间变化的映射,而映射到“在会议X的会议论文中发表的论文”是静态的。这是两个不同的资源, 即使它们在某个时间点都映射到相同的值。 区分是必要的,以便可以独立地识别和引用这两种资源。软件工程中的类似示例是在引用“最新版本”,“版本号1.2.7”或“橙色版本附带的修订版”时单独标识版本控制的源代码文件。

所以没有什么可以阻止你,正如你所说,改变根。在你的例子中,a Business 是一个价值而不是资源。创建一个资源是一个完美的RESTful,这个资源是“位于城市中的每个企业”的列表(就像Roy的例子,“橙色版本中包含的修订”),同时拥有“ID为x的资源”资源(如“修订号x”)。

对于 Employees,我会保留 API/Businesses/X7N/Employees 因为企业与员工之间的关系是一种 组成 关系,正如你所说, Employees 可以而且应该只能通过 Businesses 类根。但这不是REST要求,另一种选择也是RESTful。


注意 这符合HATEAOS原则的应用。在您的API中,位于城市中的企业列表可能(也许应该从理论的角度来看)只是一个链接列表 API/Businesses。但这意味着客户端必须为列表中的每个项目执行一次往返服务器的往返。这样做效率不高,为了保持务实,我所做的就是将业务的表示与列表一起嵌入到列表中 self 链接到此示例中的URI API/Businesses


21
2017-10-15 17:06





您不应该将REST与特定URI命名约定的应用程序混淆。

如何命名资源完全是次要的。您正在尝试使用HTTP资源命名约定 - 这与REST无关。罗伊·菲尔丁本人在其他人引用的文件中反复说明。 REST不是协议,它是一种架构风格。

事实上,罗伊菲尔丁在2008年的博客评论中说明了http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven 2012/6/20):

“REST API不能定义固定资源名称或层次结构(明显的耦合 客户端和服务器)。服务器必须能够自由控制自己的命名空间。代替, 允许服务器指示客户端如何构造适当的URI,例如在 HTML表单和URI模板,通过在媒体类型和链接关系中定义这些指令。“

所以本质上:

您描述的问题实际上并不是REST的问题 - 从概念上讲,它是层次结构与关系结构的问题。

虽然企业“在”一个城市,因此可以被认为是城市“等级”的一部分 - 那么在75个城市设有办事处的国际公司呢?然后,城市突然成为层次结构中的初级元素,其中商业名称位于结构的高级层次。

关键是,您可以从各个角度查看数据,并且根据您所采用的视点,将其视为层次结构可能最简单。但是相同的数据可以看作是具有不同级别的层次结构。使用HTTP类型资源名称时,您已输入HTTP定义的层次结构。这是一个约束,是的,但它不是REST约束,它是HTTP约束。

从这个角度来看,您可以选择更适合您场景的解决方案。如果您的客户在提供公司名称(他可能不知道)时无法提供城市名称,那么最好只使用城市名称的密钥。正如我所说,这取决于你,REST不会阻挡你...

更重要的是:

如果您已经决定在GET中使用HTTP,那么您拥有唯一真正的REST约束 PUT等等,是:

    1. 您不能在客户端和服务器之间预先知道任何先前的(“带外”)知识。 *

从这个角度看你上面的建议#1。您假设客户知道系统中包含的城市的密钥?错了 - 这不是很安静。因此,服务器必须以某种方式将城市列表作为选择列表。所以你打算列出世界上每个城市吗? 我想不是,但是你必须做一些关于你计划如何做的工作,这会带给我们:

    1. REST API应该花费几乎所有的描述性工作来定义用于表示资源和驱动应用程序状态的媒体类型......

我认为,阅读上面提到的Roy Fielding博客会对你有所帮助。


11
2017-10-17 15:46





在RESTful-API中,URL设计应该是非常不重要的 - 或者至少是一个副问题,因为可发现性是在超文本而不是URL路径中编码的。 看看链接的资源 REST标签维基 这里是StackOverflow。

但是如果你想为你的UC设计人类可读的URL,我建议如下:

  1. 使用您正在创建/更新/查询的资源类型作为URL的第一部分 (在您的API前缀之后)。因此,当某人看到该URL时,他立即知道该URL指向哪些资源。 GET /Api/Employees... 是唯一从API接收Employee资源的方法。

  2. 为每个资源使用唯一ID,与其所处的关系无关。所以 GET /Api/<CollectionType>/UniqueKey 应返回有效的资源表示。没有人应该担心员工所在的位置。 (但是返回的员工应该拥有他所属的业务链接(并且为了方便起见)。) GET /Api/Employees/Z6W 无论位于何处,都会返回具有此ID的Employee。

  3. 如果您想获得特定资源: 将查询参数放在最后(而不是问题中描述的层次结构顺序)。您可以使用URL查询字符串(GET /Api/Employees?City=X7N)或矩阵参数表达式(GET /Api/Employees;City=X7N;Business=A4X,A5Y)。这将使您能够轻松地表达特定城市中所有员工的集合 - 独立于他们所在的业务。

侧节点: 

根据我的经验,初始的分层域数据模型很少能够满足项目期间出现的额外需求。在您的情况下:考虑位于两个城市的企业。您可以通过将其建模为两个独立的业务来创建一种解决方法,但是在一个地方工作一半的员工而在另一个地方工作另一半的员工怎么办?或者更糟糕的是:只有明确他工作的业务,但它在哪个城市? 


3
2017-10-18 08:17





我看到的第三种方法是使企业和员工使用root资源并使用查询参数来过滤集合:

GET Api/Businesses?city=ABC                       (returns all Businesses in City ABC)
GET Api/Businesses/X7N                            (returns business X7N)
GET Api/Employees?businesses=X7N                  (returns all employees at business X7N)
PUT Api/Employees/WWW                             (updates employee WWW)

您的两个解决方案都使用REST子资源的概念,这要求子资源包含在父资源中,因此:

GET Api/City/ABC/Businesses

作为回应还应返回由以下提供的数据:

  GET Api/City/ABC/Businesses/X7N                 
  GET Api/City/ABC/Businesses/X7N/Employees 

类似的:

GET Api/Businesses/X7N

应返回提供的数据:

GET Api/Businesses/X7N/Employees

它会使响应的大小变大,生成所需的时间也会增加。

要使REST API清理,每个资源应该只有一个有限的URI,它可以在模式下面休息:

 GET  /resources
 GET  /resources/{id}
 POST /resources
 PUT  /resources/{id}

如果需要在资源之间建立链接使用 HATEOAS


2
2017-10-14 05:29





转到示例1.从服务器的角度来看,我不担心不必要的信息。从客户端的角度来看,URL应以独特的方式清楚地标识资源。如果客户不知道是什么 /Employee/12 意味着没有先知道它实际上是 /Businesses/X7N/Employees/12 那么第一个URL似乎是多余的。

客户端应该处理URL而不是组成URL的各个参数,因此长URL没有任何问题。对客户来说,他们只是字符串。服务器应该告诉客户端URL执行它需要做的事情,而不是那些需要客户端构造URL的单个参数。


1
2017-10-16 08:33