如何使用继承对RESTful API建模?


87

我有一个对象层次结构,我需要通过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

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

有什么意见或建议吗?

Answers:


41

我会建议:

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

为同一资源设置多个URI从来都不是一个好主意,因为它会引起混乱和意外的副作用。鉴于此,您的单个URI应该基于的通用方案/animals

借助/animalsURI方法,已经解决了在“基本”级别处理整个猫狗集合的下一个挑战。

使用媒体类型内的查询参数和标识属性的组合,可以轻松解决处理诸如猫狗之类的特殊类型的最终挑战。例如:

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方案进行任何更改。


这似乎非常类似于通过REST API进行“广播”。这也让我想起了C ++子类的内存布局中的问题/解决方案。例如,在哪里以及如何用内存中的单个地址同时表示一个基类和子类。
trcarden 2014年

10
我建议: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
dipold

1
除了将所需的类型指定为GET请求参数之外:在我看来,您也可以使用accept type来实现这一点。即: GET /animals 接受application/vnd.vet-services.animal.dog+json
BrianT。

22
如果猫和狗都具有独特的属性怎么办?您将如何在POST操作中处理该问题,因为大多数框架都不知道如何将其正确反序列化为模型,因为json不会携带良好的键入信息。您将如何处理事例[{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}
LB2

1
如果猫和狗具有(大多数)不同的属性怎么办?例如,#1发布用于SMS的通信(发送至掩码)与电子邮件(电子邮件地址,抄送,密件抄送,至,发自isHtml),或者例如#2发布用于CreditCard的资金来源(maskedPan,nameOnCard,有效期)与。一个BankAccount(bsb,accountNumber)...您仍将使用单个API资源吗?这似乎违反了SOLID原则的单一责任,但不确定这是否适用于API设计...
Ryan.Bartsch,

5

借助最新版本的OpenAPI中引入的最新增强功能,可以更好地回答此问题。

自JSON模式v1.0起,可以使用诸如oneOf,allOf,anyOf之类的关键字组合模式并获得经过验证的消息有效负载。

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

谢谢,这很好!目前看来Swagger UI并没有完全显示出来。模型将显示在底部的Schemas部分中,并且引用oneOf部分的任何响应部分都将部分显示在UI中(仅模式,没有示例),但是对于请求输入,您没有示例正文。github问题已经开放了3年,因此可能会一直保持下去:github.com/swagger-api/swagger-ui/issues/3803
Jethro

4

我会去/ 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示例应该很容易。

客户总是可以依赖在那里的“名称”元素(一个共同的属性)。但是根据“类型”属性的不同,会有其他元素作为动物表示的一部分。

返回这样的列表并没有本质上是RESTful或unRESTful的-REST没有规定用于表示数据的任何特定格式。它只是说数据必须有一些表示形式,并且该表示形式的格式由媒体类型(在HTTP中为Content-Type标头)标识。

考虑一下您的用例-您需要显示混合动物清单吗?好吧,然后返回混合动物数据列表。您只需要狗的清单吗?好吧,列出这样的清单。

相对于没有规定任何URL格式的REST,是否使用/ animals?type = dog或/ dogs无关紧要-剩下的实现细节不在REST的范围之内。REST仅声明资源应具有标识符-不用管什么格式。

您应该添加一些超级媒体链接,以更接近RESTful API。例如,通过添加对动物详细信息的引用:

<animals>
  <animal type="dog" href="https://stackoverflow.com/animals/123">
    <name>Fido</name>
    <fur-color>White</fur-color>
  </animal>
  <animal type="fish" href="https://stackoverflow.com/animals/321">
    <name>Wanda</name>
    <water-type>Salt</water-type>
  </animal>
</animals>

通过添加超媒体链接,您可以减少客户端/服务器的耦合-在上述情况下,您无需承担从客户端构建URL的负担,而是让服务器决定如何构建URL(根据定义,URL的唯一授权)。


1

但是现在,猫狗之间的关系已经消失了。

确实,但是请记住,URI根本不会反映对象之间的关系。


1

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

我总是可以说狗是动物,母鸡也是,但是母鸡会产卵,而狗是哺乳动物,所以不会。像这样的API

GET Animals /:animalID / eggs

之所以不一致,是因为它表明动物的所有亚型都可以有卵(由于Liskov替代)。如果所有的哺乳动物都对此请求做出“ 0”响应,将会有一个回退,但是如果我也启用了POST方法该怎么办?我是否应该担心明天薄饼里会有狗蛋?

处理这些情况的唯一方法是提供一个“超级资源”,该资源聚合所有可能的“派生资源”之间共享的所有子资源,然后对需要它的每个派生资源进行专业化处理,就像我们下调对象时一样进入

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

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

我错了吗?


1
我认为您过于强调URI结构。您应该能够进入“ animals /:animalID / eggs”的唯一方法是通过HATEOAS。因此,您首先要通过“ animals /:animalID”请求动物,然后对于那些可以生鸡蛋的动物,将有一个指向“ animals /:animalID / eggs”的链接,对于那些没有卵的动物,则不会成为从动物到卵子的纽带。如果有人莫名其妙地在鸡蛋不能吃鸡蛋的动物结束了,返回相应的HTTP状态代码(未找到或禁止实例)
wired_in

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

...


附加说明:GET chicken/eggs 使用控制器的通用OpenAPI代码生成器,也可以使API路由集中精力 ,但是我尚未对此进行检查。也许有人可以尝试?
Andreas Gaus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.