【问题标题】:How to model a RESTful API with inheritance?如何使用继承对 RESTful API 建模?
【发布时间】:2012-06-08 11:26:55
【问题描述】:

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

假设我有继承自 Animals 的 Dogs 和 Cats。我需要对狗和猫进行 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

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

有什么建议或建议吗?

【问题讨论】:

    标签: inheritance polymorphism rest


    【解决方案1】:

    我建议:

    • 每个资源只使用一个 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 - 得到所有的狗,只会返回雷克斯
    • GET /animals?type=cat - 得到所有的猫,只有手套

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

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

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

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

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

    【讨论】:

    • 这似乎与通过 REST API 进行“强制转换”非常相似。它还让我想起了 C++ 子类的内存布局中的问题/解决方案。例如,在哪里以及如何同时表示具有内存中单个地址的基类和子类。
    • 我建议:GET /animals - gets all dogs and catsGET /animals/dogs - gets all dogsGET /animals/cats - gets all cats
    • 除了将所需的类型指定为 GET 请求参数:在我看来,您也可以使用接受类型来实现这一点。即:GET /animals接受application/vnd.vet-services.animal.dog+json
    • 如果猫和狗各有独特的属性呢?您将如何处理 POST 操作,因为大多数框架不知道如何正确地将其反序列化为模型,因为 json 不携带良好的输入信息。您将如何处理邮政案件,例如[{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
    • 如果狗和猫有(大多数)不同的属性怎么办?例如#1 为 SMS (to, mask) 与电子邮件 (email address, cc, bcc, to, from, isHtml) 发布通信,或 eg#2 为 CreditCard (maskedPan, nameOnCard, Expiry) 与电子邮件发布 FundingSource a BankAccount (bsb, accountNumber)... 你还会使用单一的 API 资源吗?这似乎违反了 SOLID 原则的单一职责,但不确定这是否适用于 API 设计......
    【解决方案2】:

    在最新版本的 OpenAPI 中引入的最新增强支持可以更好地回答这个问题。

    可以使用 oneOf、allOf、anyOf 等关键字组合模式,并获得自 JSON 模式 v1.0 以来经过验证的消息负载。

    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
    

    【讨论】:

    • OAS 确实允许这样做。但是,不支持在 Swagger UI (link) 中显示该功能,并且我认为如果您无法向任何人展示该功能,那么它的使用是有限的。
    • @emft,不正确。在撰写此答案时,Swagger UI 已经支持。
    • 谢谢,这很好用!目前看来,Swagger UI 并没有完全显示这一点。模型将显示在底部的 Schemas 部分,任何引用 oneOf 部分的响应部分都将部分显示在 UI 中(仅模式,没有示例),但您没有得到请求输入的示例正文。这个问题的 github 问题已经开放了 3 年,所以很可能会保持这种状态:github.com/swagger-api/swagger-ui/issues/3803
    【解决方案3】:

    我会选择 /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 或非 RESTful - REST 没有规定任何特定格式来表示数据。它只是说数据必须具有 some 表示,并且该表示的格式由媒体类型标识(在 HTTP 中是 Content-Type 标头)。

    想想您的用例 - 您是否需要展示混合动物的列表?那么,然后返回一个混合动物数据的列表。你只需要一份狗的名单吗?好吧,列一个这样的清单。

    您是否使用 /animals?type=dog 或 /dogs 与没有规定任何 URL 格式的 REST 无关 - 将其作为实现细节留在 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(根据定义,它是唯一的权限) .

    【讨论】:

      【解决方案4】:

      但现在狗和猫之间的关系已经失去了。

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

      【讨论】:

        【解决方案5】:

        我知道这是一个老问题,但我有兴趣研究更多关于 RESTful 继承建模的问题

        我总是可以说狗是动物,母鸡也是,但是母鸡产蛋而狗是哺乳动物,所以它不能。类似的 API

        获取动物/:animalID/鸡蛋

        不一致,因为表明动物的所有亚型都可以有卵(作为 Liskov 替换的结果)。如果所有哺乳动物都以“0”响应此请求,将会有一个后备,但如果我也启用 POST 方法怎么办?我应该担心明天我的薄饼里会有狗蛋吗?

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

        GET /animals/:animalID/sons 获取 /hens/:animalID/eggs POST /hens/:animalID/鸡蛋

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

        我错了吗?

        【讨论】:

        • 我认为您过于强调 URI 结构。您应该能够访问“animals/:animalID/eggs”的唯一方法是通过 HATEOAS。所以你首先要通过“animals/:animalID”请求动物,然后对于那些可以有蛋的动物,会有一个指向“animals/:animalID/eggs”的链接,对于那些没有的,不会有成为从动物到鸡蛋的链接。如果有人以某种方式为没有蛋的动物买了蛋,则返回适当的 HTTP 状态代码(例如未找到或禁止)
        【解决方案6】:

        是的,你错了。也可以按照 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 代码生成器工作,但我还没有检查这个。也许有人可以试试?
        猜你喜欢
        • 2012-07-05
        • 1970-01-01
        • 2015-11-02
        • 2023-03-06
        • 1970-01-01
        • 2011-02-13
        • 1970-01-01
        • 2014-07-21
        • 1970-01-01
        相关资源
        最近更新 更多