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

39

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

为了说明概念,假设我有集合City, Business, 和 Employees。一个典型的API可能构建如下。假设ABC、X7N和WWW是键,例如guids:

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)

如您所见,我已为企业和员工创建了一个新的,尽管从域的角度来看它们是一个子/子子集合。

对我来说,两种解决方案都不太干净。

  • 第一个示例要求提供不必要的信息,但其结构方式对消费者(通过较低层次检索集合中的个别项)似乎很“自然”。
  • 第二个示例仅要求提供必要的信息,但其结构方式不太“自然”- 子集合可通过根访问。
  • 独立的员工根无法在添加新员工时使用,因为我们需要知道将员工添加到哪个企业,这意味着该调用至少必须驻留在Business根下,例如POST Api/Businesses/X7N7/Employees,这使一切变得更加混乱。

有没有一种更简洁的第三种方法,我没有想到的?

5个回答

27

我不明白REST如何添加了一项约束,使得两个资源不能具有相同的值。{resourceType/ID}只是最简单的使用情况示例,而不是从RESTful角度看待问题的最佳方式。

如果您仔细阅读Roy Fielding的论文第5.2.1.1段,您会注意到Fielding区分了资源之间的区别。现在,一个资源应该有一个唯一的URI,这是正确的。但是,没有任何东西可以防止两个资源具有相同的值:

例如,学术论文的“作者首选版本”是一个值随时间变化的映射,而“发表在X会议论文集中的论文”的映射是静态的。即使它们在某个时间点上都映射到相同的值,这些都是两个不同的资源。这种区别是必要的,以便可以独立地识别和引用两个资源。软件工程中的类似示例是,在引用“最新修订版”、“修订版号1.2.7”或“包含在Orange版本中的修订版”时,单独标识版本控制源代码文件。
因此,正如您所说,没有什么阻止您改变根。在您的示例中,“Business”是一个值而不是资源。创建一个资源列表“位于某个城市的每个企业”(就像Roy的示例“包含在Orange版本中的修订版”一样)是完全符合RESTful的,同时还有一个“ID为x的企业”资源(就像“修订版号x”一样)。
对于“员工”,我会保留“API/Businesses/X7N/Employees”作为企业和员工之间的composition关系,因此正如你所说,只能通过“Businesses”类根访问“Employees”。但这不是REST的要求,另一种替代方案也是完全符合REST原则的。
请注意,这与应用HATEAOS原则相配对。在您的API中,位于某个城市的企业列表可能(从理论上讲)只是指向API/Businesses的链接列表。但这意味着客户端需要为列表中的每个项目与服务器进行一次往返。这不是有效的解决方案,为了保持务实,我将业务表示嵌入到列表中,并附加了到此示例中的URI的self链接。

16

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

资源的名称如何命名完全是次要的。您正在尝试使用HTTP资源命名约定-这与REST没有任何关系。正如Roy Fielding先生在其他人引用的文件中反复说明的那样。REST不是一种协议,而是一种架构风格。

实际上,Roy Fielding在他2008年的博客评论中指出( http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven 6/20/2012):

“REST API不能定义固定的资源名称或层次结构(客户端和服务器之间的明显耦合)。服务器必须有自由控制自己的命名空间。相反,允许服务器通过在媒体类型和链接关系中定义这些指令来指导客户端构建适当的URI,就像在HTML表单和URI模板中所做的那样。”

因此,本质上:

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

尽管一个企业“位于”一个城市中,因此可以被认为是城市“层次结构”的一部分,但是如果一个国际公司在75个城市设有办事处,那么城市就会突然变成一个层次结构中较低的元素,而企业名称则位于该结构的高级层面。

关键是,您可以从各种角度查看数据,并且根据您采取的视角,最简单的方法可能是将其视为层次结构。但是,同一组数据可以作为具有不同级别的层次结构看待。当您使用HTTP类型资源名称时,那么您已经进入了由HTTP定义的层次结构。这是一个限制,是的,但这不是REST的限制,而是HTTP的限制。

从这个角度来看,您可以选择最适合您情况的解决方案。如果您的客户在提供公司名称时无法提供城市名称(可能不知道),则最好只使用带有城市名称的键。正如我所说的,这取决于您,REST不会阻碍您...

更重要的是:

如果您已经决定使用HTTP进行GET PUT等操作,则您拥有的唯一真正的REST约束是:

    1. 你不应该假设客户端和服务器之间有任何先前的(“out of band”)知识。*

从这个角度来看,看看您上面的建议#1。您假设客户端知道包含在系统中的城市密钥?错了-这不符合restful规范。因此,服务器必须以某种方式给出城市列表作为选择列表。那么你要在这里列出全世界的每个城市吗? 我想不是,但这会让您对计划如何完成工作进行一些工作,这将带我们到:

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

我认为阅读Roy Fielding博客中提到的内容会对您有很大帮助。


5
在RESTful-API的URL设计中,URL路径应该是不太重要的,或者至少是次要的,因为可发现性被编码在超文本中而不是URL路径中。请参考StackOverflow上此处的REST标签wiki中链接的资源。
但是,如果您想为您的UC设计易读的URL,请按照以下建议:
1. 在URL的第一部分(在API前缀之后)使用正在创建/更新/查询的资源类型。这样,当有人看到URL时,他立即知道此URL指向哪些资源。例如,GET /Api/Employees... 是从API接收Employee资源的唯一方式。
2. 对于每个资源使用唯一ID,独立于它们所在的关系。因此,GET /Api/<CollectionType>/UniqueKey 应返回有效的资源表示。没有人需要担心Employee位于何处。(但是返回的Employee应具有他所属的Business(和方便起见的City)的链接。)例如,GET /Api/Employees/Z6W 返回此ID的Employee,无论它位于何处。
3. 如果您想获取特定资源:请将查询参数放在末尾(而不是按照问题中描述的分层顺序)。您可以使用URL查询字符串(GET /Api/Employees?City=X7N)或矩阵参数表达式(GET /Api/Employees;City=X7N;Business=A4X,A5Y)。这将使您能够轻松地表示特定城市中所有员工的集合,而与他们所在的业务无关。
附注:
根据我的经验,初始层次结构域数据模型很少能够在项目期间出现的其他要求中生存。在您的情况下:考虑一个位于两个城市的企业。您可以通过将其建模为两个单独的企业来创建解决方法,但是对于在一个地方工作一半时间,在另一个位置工作另一半时间的员工怎么办?或者更糟糕的是:很明显他在哪个企业工作,但是在哪个城市却未定义?

3
我看到的第三种方法是将企业和员工作为根资源,并使用查询参数来过滤集合:
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


1

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

客户端应处理URL而不是组成URL的各个参数,因此长URL并没有问题。对于客户端来说,它们只是字符串。服务器应该告诉客户端需要做什么URL,而不是单独的参数,然后要求客户端构建URL。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接