如何用继承建模RESTful API?

108

我有一个对象层次结构,需要通过RESTful API公开,但我不确定URL应该如何构建以及它们应该返回什么。我找不到最佳实践。

假设我有从动物继承的狗和猫。 我需要对狗和猫执行CRUD操作; 我还希望能够对一般动物进行操作。

我的第一个想法是这样的:

GET /animals        # get all animals
POST /animals       # create a dog or cat
GET /animals/123    # get animal 123

问题在于/animals集合现在是“不一致的”,因为它可以返回和获取结构不完全相同的对象(狗和猫)。如果一个集合可以返回具有不同属性的对象,这是否被认为是“符合REST原则”的?

另一种解决方案是为每个具体类型创建一个URL,像这样:

GET /dogs       # get all dogs
POST /dogs      # create a dog
GET /dogs/123   # get dog 123

GET /cats       # get all cats
POST /cats      # create a cat
GET /cats/123   # get cat 123

但是现在狗和猫之间的关系已经丢失。如果想要检索所有动物,则必须查询狗和猫资源。每增加一种新的动物亚种,URL的数量也会增加。

另一个建议是通过添加以下内容来增强第二个解决方案:

GET /animals    # get common attributes of all animals
在这种情况下,返回的动物仅包含所有动物共有的属性,舍弃了狗和猫特有的属性。这样可以检索到所有动物,但细节会减少。每个返回的对象都可以包含到详细、具体版本的链接。
有任何评论或建议吗?
6个回答

47

我的建议是:

  • 每个资源只使用一个URI
  • 仅在属性级别区分动物

将多个URI设置为同一资源从来不是一个好主意,因为它可能会导致混淆和意外的副作用。因此,您的单个URI应基于通用方案,如/animals

处理整个狗和猫集合的下一个挑战已经通过使用/animals URI方法解决。

处理像狗和猫这样的专业类型的最终挑战可以通过在媒体类型中使用查询参数和识别属性的组合来轻松解决。例如:

GET /animals (Accept : application/vnd.vet-services.animals+json)

{
   "animals":[
      {
         "link":"/animals/3424",
         "type":"dog",
         "name":"Rex"
      },
      {
         "link":"/animals/7829",
         "type":"cat",
         "name":"Mittens"
      }
   ]
}
  • GET /animals - 获取所有狗和猫,将返回Rex和Mittens。
  • GET /animals?type=dog - 获取所有狗,只返回Rex。
  • GET /animals?type=cat - 获取所有猫,只返回Mittens。

当创建或修改动物时,调用者需要指定所涉及的动物类型:

媒体类型:application/vnd.vet-services.animal+json

{
   "type":"dog",
   "name":"Fido"
}

可以使用 POSTPUT 请求发送上述有效负载。

通过REST,上述方案为您提供基本的类似于OO继承的特性,并且具有添加进一步专业化(即更多动物类型)的能力,而无需进行大规模手术或对URI方案进行任何更改。


这似乎与通过REST API进行的“转换”非常相似。它也让我想起了C ++子类的内存布局中的问题/解决方案。例如,在内存中同时表示基类和子类的位置和方式。 - trcarden
10
我建议:
GET /animals - 获取所有的狗和猫
GET /animals/dogs - 获取所有的狗
GET /animals/cats - 获取所有的猫
- dipold
1
除了在GET请求参数中指定所需的类型之外:我认为您也可以使用接受类型来实现这一点。也就是说:GET /animals 接受 application/vnd.vet-services.animal.dog+json - BrianT.
32
如果猫和狗各自具有独特的属性,那该怎么处理POST操作呢?因为大多数框架无法从Json中获取好的类型信息,所以无法正确地将其反序列化为模型。比如,对于以下的请求数据 [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}],你该如何处理? - LB2
1
如果狗和猫具有(大多数)不同的属性会怎样呢?例如#1:发送短信(收件人,掩码)与发送电子邮件(电子邮件地址,抄送,密送,收件人,发件人,isHtml);或者例如#2:提交信用卡资金来源(掩码PAN,卡上姓名,到期日)与银行账户(BSB,账号)...您是否仍然会使用单个API资源?这似乎违反了SOLID原则中的单一责任,但不确定是否适用于API设计... - Ryan.Bartsch
显示剩余3条评论

11

在撰写本文时,最新版本的OpenAPI是v3。使用最新版本引入的增强功能可以更好地回答这个问题。

自JSON schema v1.0以来,已经可以使用关键字(如oneOfallOfanyOf)组合模式并验证消息负载。

https://spacetelescope.github.io/understanding-json-schema/reference/combining.html

然而,在OpenAPI(原Swagger)中,通过关键字discriminator(v2.0+)和oneOf(v3.0+)增强了模式组合,以真正支持多态性。

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaComposition

您可以使用oneOf(选择一个子类型)和allOf(组合类型及其某个子类型)来建模您的继承。以下是POST方法的示例定义。

paths:
  /animals:
    post:
      requestBody:
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/Dog'
              - $ref: '#/components/schemas/Cat'
              - $ref: '#/components/schemas/Fish'
            discriminator:
              propertyName: animal_type
     responses:
       '201':
         description: Created

components:
  schemas:
    Animal:
      type: object
      required:
        - animal_type
        - name
      properties:
        animal_type:
          type: string
        name:
          type: string
      discriminator:
        property_name: animal_type
    Dog:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            playsFetch:
              type: string
    Cat:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            likesToPurr:
              type: string
    Fish:
      allOf:
        - $ref: "#/components/schemas/Animal"
        - type: object
          properties:
            water-type:
              type: string

1
确实,OAS允许这样做。然而,在Swagger UI中没有支持该功能的显示(链接),如果您无法向任何人展示它,我认为这个功能的用处很有限。 - emft
1
@emft,不是的。在写下这个答案的时候,Swagger UI已经支持了该功能。 - Andrejs Cainikovs
1
谢谢,这很有效!不过目前似乎Swagger UI并没有完全显示它。模型将显示在底部的架构部分中,任何引用oneOf部分的响应部分将在UI中部分显示(仅架构,没有示例),但您无法获得请求输入的示例主体。该问题在Github上已经存在了3年,所以可能会一直这样:https://github.com/swagger-api/swagger-ui/issues/3803 - Jethro

4
我会选择/动物返回狗和鱼以及其他任何东西的列表:
<animals>
  <animal type="dog">
    <name>Fido</name>
    <fur-color>White</fur-color>
  </animal>
  <animal type="fish">
    <name>Wanda</name>
    <water-type>Salt</water-type>
  </animal>
</animals>

应该很容易实现类似的JSON示例。
客户端始终可以依赖于“name”元素的存在(这是一个常见的属性)。但是,根据“type”属性,动物表示中将有其他元素。
在返回这样的列表方面,没有任何固有的RESTful或非RESTful - REST不规定表示数据的特定格式。它只是说数据必须具有某种表示形式,并且该表示形式的格式由媒体类型确定(在HTTP中为Content-Type标头)。
考虑您的用例 - 您需要显示混合动物的列表吗?那么返回混合动物数据的列表。您需要仅狗的列表吗?好吧,制作这样的列表。
无论您是执行/animals?type=dog还是/dogs都与REST无关,因为REST不规定任何URL格式 - 这留给REST范围之外的实现细节。 REST仅指出资源应具有标识符 - 不要紧怎样。
您应该添加一些超媒体链接以更接近RESTful API。例如,通过添加对动物详细信息的引用:
<animals>
  <animal type="dog" href="/animals/123">
    <name>Fido</name>
    <fur-color>White</fur-color>
  </animal>
  <animal type="fish" href="/animals/321">
    <name>Wanda</name>
    <water-type>Salt</water-type>
  </animal>
</animals>

通过添加超媒体链接,您可以减少客户端/服务器耦合 - 在上述情况下,您将 URL 构建的负担从客户端中移除,并让服务器决定如何构建 URL(这是它唯一的权威)。

1

我知道这是一个老问题,但我对研究RESTful继承建模的更深层次问题感兴趣。

我可以说狗和鸡都是动物,但鸡会下蛋而狗是哺乳动物,所以它不能。像

GET animals/:animalID/eggs

这样的API不一致,因为它表明所有动物的子类型都可以有蛋(作为Liskov替换原则的结果)。如果所有哺乳动物都对此请求回复“0”,那么就会有一个后备方案,但如果我还启用了POST方法呢?我应该担心明天我的可丽饼里会有狗蛋吗?

处理这些情况的唯一方法是提供一个“超级资源”,其中聚合了所有可能的“派生资源”共享的子资源,然后为每个需要它的派生资源提供专门化,就像我们将对象向下转换为oop时一样

GET /animals/:animalID/sons GET /hens/:animalID/eggs POST /hens/:animalID/eggs

这里的缺点是,有人可能会传递一个狗ID来引用母鸡集合的实例,但狗不是母鸡,所以如果响应是404或带有原因消息的400,那么这将不正确。

我错了吗?


2
我认为你过于强调URI结构。你唯一能够访问"animals/:animalID/eggs"的方式是通过HATEOAS。因此,你首先需要通过"animals/:animalID"请求动物,然后对于那些可以产蛋的动物,会有一个指向"animals/:animalID/eggs"的链接,而对于那些不能产蛋的动物,则没有从动物到蛋的链接。如果某人以某种方式进入了不能产蛋的动物的蛋页面,则返回适当的HTTP状态码(例如未找到或禁止)。 - wired_in

1
“但现在狗和猫之间的关系已经失去了。” “确实如此,但请记住URI从根本上不反映对象之间的关系。”

0

是的,你错了。同时,关系也可以按照OpenAPI规范进行建模,例如采用这种多态方式。

Chicken:
  type: object
  discriminator:
    propertyName: typeInformation
  allOf:
    - $ref:'#components/schemas/Chicken'
    - type: object
      properties:
        eggs:
          type: array
          items:
            $ref:'#/components/schemas/Egg'
          name:
            type: string

...


附加评论:专注于API路由GET chicken/eggs使用常见的OpenAPI代码生成器也应该可以为控制器工作,但我还没有检查过。也许有人可以试试? - Andreas Gaus

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