Как смоделировать 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

В этом случае возвращенные животные будут содержать только атрибуты, общие для всех животных, без атрибутов, характерных для собак и кошек. Это позволяет получить всех животных, хотя и с меньшим количеством деталей. Каждый возвращаемый объект может содержать ссылку на подробную конкретную версию.

Есть комментарии или предложения?

Альфа Гидра
источник

Ответы:

41

Я бы посоветовал:

  • Использование только одного 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 - получает всех собак и кошек, вернет и Рекса, и варежек
  • GET /animals?type=dog - получает всех собак, вернет только Рекса
  • GET /animals?type=cat - заводит всех кошек, бы только варежки

Затем при создании или изменении животных вызывающий должен будет указать тип задействованного животного:

Тип СМИ: application/vnd.vet-services.animal+json

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

Вышеуказанная полезная нагрузка может быть отправлена ​​с помощью запроса POSTили PUT.

Вышеупомянутая схема дает вам основные характеристики, аналогичные OO-наследованию через REST, и с возможностью добавления дополнительных специализаций (т.е. большего количества типов животных) без серьезных операций или каких-либо изменений в вашей схеме URI.

Брайан Келли
источник
Это похоже на «кастинг» через REST API. Это также напоминает мне о проблемах / решениях в структуре памяти подкласса C ++. Например, где и как одновременно представлять базовый класс и подкласс с одним адресом в памяти.
trcarden
10
Предлагаю: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
дипольд
1
В дополнение к указанию желаемого типа в качестве параметра запроса GET: мне кажется, вы также можете использовать тип accept для достижения этого. То есть: 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 (на, маска) по сравнению с электронной почтой (адрес электронной почты, cc, bcc, to, from, isHtml) или, например, # 2 POSTing a FundingSource for CreditCard (maskedPan, nameOnCard, Expiry) vs. a BankAccount (bsb, accountNumber) ... вы бы по-прежнему использовали единственный ресурс API? Казалось бы, это нарушает единую ответственность принципов SOLID, но не уверен, относится ли это к дизайну API ...
Райан Барч
5

На этот вопрос можно лучше ответить с помощью недавнего улучшения, представленного в последней версии OpenAPI.

Было возможно комбинировать схемы, используя такие ключевые слова, как oneOf, allOf, anyOf, и получать полезные данные сообщения, проверенные с момента появления схемы 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
dbaltor
источник
1
Верно, что OAS позволяет это. Однако нет поддержки функции, которая будет отображаться в пользовательском интерфейсе Swagger ( ссылка ), и я думаю, что функция имеет ограниченное использование, если вы не можете показать ее кому-либо.
emft 08
1
@emft, неправда. На момент написания этого ответа пользовательский интерфейс Swagger уже поддерживает это.
Андрей Кайников
Спасибо, отлично работает! В настоящее время кажется правдой, что пользовательский интерфейс Swagger не отображает это полностью. Модели будут отображаться в разделе «Схемы» внизу, и любой раздел ответов, ссылающийся на раздел oneOf, будет частично отображаться в пользовательском интерфейсе (только схема, без примеров), но вы не получите пример тела для ввода запроса. Проблема с 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).

Подумайте о своих сценариях использования - вам нужно показать список смешанных животных? Что ж, тогда верните список смешанных данных о животных. Вам нужен только список собак? Что ж, составьте такой список.

Независимо от того, используете ли вы / animals? Type = dog или / dogs, это не имеет отношения к REST, который не предписывает какие-либо форматы URL-адресов - это оставлено в качестве детали реализации за пределами 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-адреса (которые он по определению является единственным органом власти).

Йорн Вильдт
источник
1

Но теперь отношения между собаками и кошками потеряны.

Действительно, но имейте в виду, что URI просто никогда не отражает отношения между объектами.

Г. Демеки
источник
1

Я знаю, что это старый вопрос, но мне интересно изучить дальнейшие вопросы моделирования наследования RESTful.

Я всегда могу сказать, что собака - это животное, и курица тоже, но курица откладывает яйца, а собака - млекопитающее, поэтому не может. API вроде

ПОЛУЧИТЬ животные /: animalID / яйца

не согласуется, потому что указывает на то, что все подтипы животных могут иметь яйца (как следствие замещения Лискова). Если все млекопитающие ответят «0» на этот запрос, произойдет откат, но что, если я также включу метод POST? Стоит ли бояться, что завтра в моих блинчиках будут собачьи яйца?

Единственный способ справиться с этими сценариями - предоставить `` суперресурс '', который объединяет все подресурсы, общие для всех возможных `` производных ресурсов '', а затем специализацию для каждого производного ресурса, который в нем нуждается, точно так же, как когда мы понижаем объект в уп

GET / animals /: animalID / sons GET / hens /: animalID / яйца POST / hens /: animalID / яйца

Недостатком здесь является то, что кто-то может передать идентификатор собаки для ссылки на экземпляр коллекции кур, но собака не является курицей, поэтому было бы неправильно, если бы ответ был 404 или 400 с сообщением о причине.

Я ошибся?

Кармин Ингальди
источник
1
Я думаю, вы слишком много внимания уделяете структуре URI. Единственный способ получить доступ к "animals /: animalID / яйца" - это HATEOAS. Итак, вы сначала запросите животное через «animals /: animalID», а затем для тех животных, у которых могут быть яйца, будет ссылка на «animals /: animalID / яйца», а для тех, у которых нет, не будет быть ссылкой для перехода от животного к яйцу. Если кто-то каким-то образом попадает в яйца для животного, которое не может иметь яиц, верните соответствующий код статуса 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

...

Андреас Гаус
источник
Дополнительный комментарий: фокусировка маршрута API GET chicken/eggs также должна работать с использованием общих генераторов кода OpenAPI для контроллеров, но я еще не проверял это. Может кто попробует?
Андреас Гаус