Як змоделювати 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запитом.

Наведена схема дає вам основні подібні характеристики, як наслідування ОО через 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
dipold

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 (до, маски) порівняно з електронною поштою (електронна адреса, копія, прихована копія, від, isHtml), або, наприклад, №2 ОПУБЛІКУВАННЯ джерела фінансування для CreditCard (maskedPan, nameOnCard, Expiry) проти. 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 (колишній Сваггер) склад схем був покращений за допомогою ключових слів дискримінатор (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 ( посилання ) не підтримується, і я думаю, що функція обмежена, якщо ви не можете її нікому показати.
emft

1
@emft, неправда. На момент написання цієї відповіді інтерфейс Swagger вже підтримує це.
Андрей Кайніков,

Дякую, це чудово працює! На сьогодні справді здається, що інтерфейс Swagger не показує цього повністю. Моделі відображатимуться в розділі "Схеми" внизу, а будь-який розділ відповідей, що посилається на розділ oneOf, частково відображатиметься в інтерфейсі (лише схема, без прикладів), але ви не отримаєте жодного прикладу для введення запиту. Випуск github для цього був відкритий протягом 3 років, тому, ймовірно, так і залишиться: github.com/swagger-api/swagger-ui/issues/3803
Jethro

4

Я б пішов на те, щоб / тварини повертали список як собак, так і риб, а також що-небудь ще:

<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 = 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? Чи варто боятися, що завтра в моїх блинах будуть собачі яйця?

Єдиний спосіб обробити ці сценарії - це надати "суперресурс", який об'єднує всі підресурси, що діляться між усіма можливими "похідними ресурсами", а потім спеціалізацію для кожного похідного ресурсу, який у цьому потребує, як коли ми знижуємо об'єкт в oop

GET / тварини /: animalID / сини GET / кури /: animalID / яйця POST / кури /: animalID / яйця

Тут недоліком є ​​те, що хтось міг передати ідентифікатор собаки для посилання на екземпляр збору курей, але собака не є квочкою, тому це не буде неправильним, якщо відповідь буде 404 або 400 із повідомленням про причину

Я помиляюся?


1
Я думаю, ви занадто акцентуєте увагу на структурі URI. Єдиний шлях, яким ви зможете дістатися до "тварини /: 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 для контролерів, але я ще не перевіряв це. Може хтось може спробувати?
Андреас Гаус,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.