如何使用继承对 RESTful API 进行建模



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

假设我有从动物那里继承的狗和猫。我需要对狗和猫进行 CRUD 手术;我也希望能够对动物进行一般手术。

我的第一个想法是做这样的事情:

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

问题是/animals 集合现在是"不一致的",因为它可以返回并获取结构不完全相同的对象(狗和猫)。让集合返回具有不同属性的对象是否被视为"RESTful"?

另一种解决方案是为每个具体类型创建一个 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

在这种情况下,返回的动物将仅包含所有动物共有的属性,删除特定于狗和特定于猫的属性。这允许检索所有动物,尽管细节较少。每个返回的对象都可以包含指向详细具体版本的链接。

有什么意见或建议吗?

我建议:

  • 每个资源仅使用一个 URI
  • 仅在属性层面区分动物

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

"基本"级别处理整个狗和猫集合的下一个挑战已经通过/animals URI方法解决了。

处理狗和猫等特殊类型的最后一个挑战可以通过在媒体类型中组合查询参数和标识属性轻松解决。例如:

GET /animalsAccept : application/vnd.vet-services.animals+json

{
   "animals":[
      {
         "link":"/animals/3424",
         "type":"dog",
         "name":"Rex"
      },
      {
         "link":"/animals/7829",
         "type":"cat",
         "name":"Mittens"
      }
   ]
}
  • GET /animals - 得到所有的狗和猫,会返回雷克斯和手套
  • GET /animals?type=dog - 得到所有的狗,只会返回雷克斯
  • GET /animals?type=cat - 得到所有的猫,只会连指手套

然后,在创建或修改动物时,调用方有责任指定所涉及的动物类型:

介质类型:application/vnd.vet-services.animal+json

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

上述有效负载可以通过POSTPUT请求发送。

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

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

可以使用

oneOfallOfanyOf 等关键字组合架构,并获得自 JSON 架构 v1.0 以来验证的消息有效负载。

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

但是,在OpenAPI(以前的Swagger)中,通过关键字鉴别器(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

我会选择/animals返回狗和鱼的列表以及其他任何东西:

<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"元素(一个公共属性)。但是根据"类型"属性,动物表示中还会有其他元素。

在返回这样的列表时,没有什么固有的 RESTful 或 unRESTful - 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(根据定义,它是唯一的权威)。

但现在狗和猫的关系消失了。

确实如此,但请记住,URI 从不反映对象之间的关系。

我知道

这是一个老问题,但我有兴趣调查有关 RESTful 继承建模的进一步问题

我总是可以说狗是动物和母鸡,但母鸡会下蛋,而狗是哺乳动物,所以它不能。像这样的 API

获取动物/

:动物ID/蛋

不一致,因为表明动物的所有亚型都可以有卵(作为Liskov替代的结果)。如果所有哺乳动物都对此请求响应"0",则会有一个回退,但是如果我也启用 POST 方法怎么办?我应该害怕明天我的薄饼里会有狗蛋吗?

处理这些场景的唯一方法是提供一个"超级资源",该资源聚合所有可能的"派生资源"之间共享的所有子资源,然后为每个需要它的派生资源进行专用化,就像我们将一个对象向下转换为 oop 一样。

获取/animals/:animalID/sons获取/母鸡/:动物ID/鸡蛋帖子/母鸡/:动物ID/鸡蛋

这里的缺点是有人可以传递狗 ID 来引用母鸡集合的实例,但狗不是母鸡,因此如果响应是 404 或 400 并带有原因消息,则不会不正确

我错了吗?

是的,你错了。此外,还可以按照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

最新更新