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

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

Любые комментарии или предложения?

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

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

Тип носителя: application/vnd.vet-services.animal+json

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

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

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

Ответ 2

Я пошел бы за/животными, возвращающими список как собак, так и рыб, и что еще:

<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 или unRESTful - REST не задает какой-либо конкретный формат представления данных. Все, что он говорит, состоит в том, что данные должны иметь некоторое представление, а формат для этого представления идентифицируется типом носителя (который в HTTP является заголовком Content-Type).

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

Если вы делаете/животные? type = собака или/собака не имеет отношения к REST, который не задает никаких форматов URL-адресов, которые остаются в качестве детали реализации вне рамок REST. REST только заявляет, что ресурсы должны иметь идентификаторы - неважно, в каком формате.

Вы должны добавить ссылку на гиперссылку, чтобы приблизиться к API RESTful. Например, добавив ссылки на детали животных:

<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-адреса (которые по определению являются единственными полномочиями).

Ответ 3

На этот вопрос можно лучше ответить с поддержкой недавнего усовершенствования, представленного в последней версии 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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

GET/животные/: animalID/сыновья GET/курицы/: animalID/яйца POST/куры/: animalID/яйца

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

Я ошибся?