39

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

为了演示这个概念,假设我有集合CityBusinessEmployees。典型的 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)

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

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

  • 第一个示例要求提供不必要的信息,但其结构对消费者来说似乎是“自然的”(集合中的单个项目通过较低的叶子检索)
  • 第二个示例只要求提供必要的信息,但不是以“自然”方式构建的——子集合可以通过根访问
  • 添加新员工时,单个员工根将不起作用,因为我们需要知道将员工添加到哪个业务,这意味着调用至少必须驻留在业务根下,例如POST Api/Businesses/X7N7/Employees,这使得一切更加混乱.

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

4

5 回答 5

27

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

如果您仔细阅读Roy Fielding 论文的第 5.2.1.1 段,您会注意到 Fielding 区分了valueresource。现在一个资源应该有一个唯一的 URI,这是真的。但没有什么能阻止两个资源具有相同的价值:

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

因此,正如您所说,没有什么可以阻止您更改根目录。在您的示例中, aBusiness是一个值而不是资源。创建一个资源是完全 RESTful 的,它是“位于城市中的每个企业”的列表(就像 Roy 的示例,“Orange 版本中包含的修订”),同时还有一个“ID 为 x 的企业”资源(如“修订号 x”)。

因为Employees,我认为API/Businesses/X7N/Employees企业与其员工之间的关系是一种组合关系,因此正如您所说,Employees只能通过Businesses类根访问。但这不是 REST 要求,另一种选择也是完全 RESTful 的。


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

于 2013-10-15T17:06:54.630 回答
16

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

如何命名资源完全是次要的。您正在尝试使用 HTTP 资源命名约定 - 这与 REST 无关。罗伊菲尔丁本人在上述其他人引用的文件中反复声明。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. 您不应假定客户端和服务器之间有任何先前的(“带外”)知识。*

从这个角度来看你的建议#1。您假设客户知道您系统中包含的城市的密钥吗?错了——那是不平静的。所以服务器必须以某种方式将城市列表作为选择列表。那么你要在这里列出世界上的每个城市吗?我猜不是,但是你必须做一些关于你计划如何做到这一点的工作,这让我们:

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

我认为,阅读上面提到的 Roy Fielding 博客将对您有很大帮助。

于 2013-10-17T15:46:12.437 回答
5

在 RESTful-API 中,URL 设计应该是相当不重要的——或者至少是一个附带问题,因为可发现性是在超文本中编码的,而不是在 URL 路径中。在 StackOverflow 上查看REST 标签 wiki中链接的资源。

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

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

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

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

侧节点:

以我的经验,最初的分层域数据模型很少能经受住项目期间出现的额外要求。在您的情况下:考虑位于两个城市的企业。您可以通过将其建模为两个独立的业务来创建一种解决方法,但是对于一半时间在一个地方工作而另一半时间在另一个地方工作的员工呢?或者更糟糕的是:只知道他在哪个行业工作,但不确定,在哪个城市?

于 2013-10-18T08:17:38.677 回答
3

我看到的第三种方法是让 Businesses 和 Employees 根资源并使用查询参数来过滤集合:

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

于 2013-10-14T05:29:53.333 回答
1

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

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

于 2013-10-16T08:33:04.597 回答