Jak modelować RESTful API z dziedziczeniem?

88

Mam hierarchię obiektów, którą muszę ujawnić za pośrednictwem interfejsu API RESTful i nie jestem pewien, jak powinny być zbudowane moje adresy URL i co powinny zwracać. Nie udało mi się znaleźć żadnych najlepszych praktyk.

Powiedzmy, że mam psy i koty dziedziczące po zwierzętach. Potrzebuję operacji CRUD na psach i kotach; Chcę też mieć możliwość wykonywania ogólnych operacji na zwierzętach.

Moim pierwszym pomysłem było zrobienie czegoś takiego:

GET /animals        # get all animals
POST /animals       # create a dog or cat
GET /animals/123    # get animal 123

Chodzi o to, że kolekcja / animals jest teraz „niespójna”, ponieważ może zwracać i przyjmować obiekty, które nie mają dokładnie tej samej struktury (psy i koty). Czy kolekcję zwracającą obiekty o różnych atrybutach uważa się za „RESTful”?

Innym rozwiązaniem byłoby utworzenie adresu URL dla każdego konkretnego typu, na przykład:

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

Ale teraz związek między psami i kotami jest stracony. Jeśli ktoś chce odzyskać wszystkie zwierzęta, należy zapytać zarówno o zasoby psa, jak i kota. Liczba adresów URL również będzie wzrastać z każdym nowym podtypem zwierzęcia.

Inną sugestią było rozszerzenie drugiego rozwiązania poprzez dodanie tego:

GET /animals    # get common attributes of all animals

W takim przypadku zwrócone zwierzęta będą zawierały tylko atrybuty wspólne dla wszystkich zwierząt, z pominięciem atrybutów specyficznych dla psów i kotów. Pozwala to na odzyskanie wszystkich zwierząt, choć z mniejszą liczbą szczegółów. Każdy zwracany obiekt może zawierać link do szczegółowej, konkretnej wersji.

Jakieś uwagi lub sugestie?

Alpha Hydrae
źródło

Odpowiedzi:

41

Sugerowałbym:

  • Korzystanie tylko z jednego identyfikatora URI na zasób
  • Rozróżnianie zwierząt wyłącznie na poziomie atrybutów

Skonfigurowanie wielu identyfikatorów URI dla tego samego zasobu nigdy nie jest dobrym pomysłem, ponieważ może powodować zamieszanie i nieoczekiwane skutki uboczne. Biorąc to pod uwagę, Twój pojedynczy identyfikator URI powinien być oparty na ogólnym schemacie, takim jak /animals.

Kolejne wyzwanie, jakim jest radzenie sobie z całą kolekcją psów i kotów na poziomie „podstawowym”, zostało już rozwiązane dzięki /animalspodejściu URI.

Ostatnie wyzwanie związane z obsługą wyspecjalizowanych typów, takich jak psy i koty, można łatwo rozwiązać za pomocą kombinacji parametrów zapytania i atrybutów identyfikacyjnych w typie mediów. Na przykład:

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 - dostaje wszystkie psy i koty, zwróci zarówno Rexa, jak i rękawiczki
  • GET /animals?type=dog - dostaje wszystkie psy, zwróci tylko Rexa
  • GET /animals?type=cat - dostaje wszystkie koty, tylko Rękawiczki

Następnie, podczas tworzenia lub modyfikowania zwierząt, na dzwoniącym spoczywałby obowiązek określenia typu zwierzęcia:

Typ mediów: application/vnd.vet-services.animal+json

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

Powyższy ładunek można wysłać z żądaniem POSTlub PUT.

Powyższy schemat zapewnia podstawowe cechy podobne do dziedziczenia OO przez REST, z możliwością dodawania dalszych specjalizacji (tj. Większej liczby typów zwierząt) bez poważnych operacji lub zmian w schemacie URI.

Brian Kelly
źródło
Wydaje się to bardzo podobne do „przesyłania” przez REST API. Przypomina mi również o problemach / rozwiązaniach w układzie pamięci podklasy C ++. Na przykład gdzie i jak oba reprezentują jednocześnie bazę i podklasę z jednym adresem w pamięci.
trcarden
10
Proponuję: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
dipold
1
Oprócz określenia żądanego typu jako parametru żądania GET: wydaje mi się, że można by to osiągnąć również przy użyciu typu accept. To znaczy: GET /animals Akceptujapplication/vnd.vet-services.animal.dog+json
BrianT.
22
A co jeśli kot i pies mają wyjątkowe właściwości? Jak poradzisz sobie z tym podczas POSToperacji, ponieważ większość frameworków nie wiedziałaby, jak prawidłowo deserializować go do modelu, ponieważ json nie zawiera dobrych informacji o wpisywaniu. Jak radziłbyś sobie np. Ze sprawami pocztowymi [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
LB2
1
A gdyby tak psy i koty miały (w większości) różne właściwości? np. # 1 WYSŁANIE KOMUNIKATU SMS (do, maska) vs. e-mail (adres e-mail, DW, UDW, do, od, isHtml) lub np. # 2 POSTING źródła finansowania karty kredytowej (maskedPan, nameOnCard, Expiry) vs. BankAccount (bsb, accountNumber) ... czy nadal używałbyś jednego zasobu API? Wydawałoby się, że narusza to odpowiedzialność wynikającą z zasad SOLID, ale nie jestem pewien, czy dotyczy to projektowania API ...
Ryan.Bartsch
5

Lepszą odpowiedź na to pytanie można uzyskać, korzystając z niedawnego ulepszenia wprowadzonego w najnowszej wersji OpenAPI.

Możliwe było łączenie schematów za pomocą słów kluczowych, takich jak oneOf, allOf, anyOf i uzyskiwanie ładunku wiadomości zweryfikowanego od wersji 1.0 schematu JSON.

https://spacetelescope.github.io/understanding-json-schema/reference/combining.html

Jednak w OpenAPI (dawniej Swagger) skład schematów został ulepszony przez słowa kluczowe dyskryminator (v2.0 +) i oneOf (v3.0 +), aby naprawdę wspierać polimorfizm.

https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaComposition

Twoje dziedzictwo może być modelowane przy użyciu kombinacji oneOf (w celu wybrania jednego z podtypów) i allOf (w celu połączenia typu i jednego z jego podtypów). Poniżej znajduje się przykładowa definicja metody 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
źródło
1
To prawda, że ​​OAS na to pozwala. Jednak nie ma wsparcia dla funkcji, która ma być wyświetlana w interfejsie użytkownika Swagger ( link ) i myślę, że funkcja ma ograniczone zastosowanie, jeśli nie możesz jej nikomu pokazać.
emft
1
@emft, nieprawda. W chwili pisania tej odpowiedzi interfejs Swagger już to obsługuje.
Andrejs Cainikovs
Dzięki, to działa świetnie! Obecnie wydaje się prawdą, że interfejs Swagger nie pokazuje tego w pełni. Modele zostaną wyświetlone w sekcji Schematy u dołu, a wszelkie sekcje odpowiedzi odwołujące się do sekcji oneOf będą częściowo wyświetlane w interfejsie użytkownika (tylko schemat, bez przykładów), ale nie otrzymasz przykładowej treści dla danych wejściowych żądania. Problem na githubie
Jethro
4

Poszedłbym po / zwierzęta zwracając listę zarówno psów, jak i ryb i co jeszcze:

<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>

Wdrożenie podobnego przykładu JSON powinno być łatwe.

Klienci zawsze mogą polegać na obecnym elemencie „name” (wspólnym atrybucie). Ale w zależności od atrybutu „typ” będą inne elementy jako część reprezentacji zwierzęcia.

W zwracaniu takiej listy nie ma nic z natury RESTful ani unRESTful - REST nie określa żadnego konkretnego formatu reprezentacji danych. Mówi się tylko, że dane muszą mieć jakąś reprezentację, a format tej reprezentacji jest identyfikowany przez typ nośnika (który w HTTP jest nagłówkiem Content-Type).

Pomyśl o swoich przypadkach użycia - czy musisz pokazać listę zwierząt mieszanych? W takim razie zwróć listę mieszanych danych o zwierzętach. Czy potrzebujesz tylko listy psów? Cóż, zrób taką listę.

Czy robisz / animals? Type = dog czy / dogs nie ma znaczenia w odniesieniu do REST, który nie narzuca żadnych formatów URL - to jest pozostawione jako szczegół implementacji poza zakresem REST. REST tylko stwierdza, że ​​zasoby powinny mieć identyfikatory - nieważne w jakim formacie.

Aby zbliżyć się do interfejsu API RESTful, należy dodać kilka hiperłączy. Na przykład dodając odniesienia do szczegółów dotyczących zwierząt:

<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>

Dodając hiper media link, zmniejszasz sprzężenie klient / serwer - w powyższym przypadku zdejmujesz ciężar budowy URL z klienta i pozwalasz serwerowi decydować o tym, jak konstruować adresy URL (które z definicji jest jedynym autorytetem).

Jørn Wildt
źródło
1

Ale teraz związek między psami i kotami jest stracony.

Rzeczywiście, ale pamiętaj, że URI po prostu nigdy nie odzwierciedla relacji między obiektami.

G. Demecki
źródło
1

Wiem, że to stare pytanie, ale jestem zainteresowany zbadaniem dalszych problemów dotyczących modelowania dziedziczenia RESTful

Zawsze mogę powiedzieć, że pies to zwierzę i kura też, ale kura robi jaja, a pies to ssak, więc nie może. API jak

POBIERZ zwierzęta /: identyfikator zwierzęcia / jaja

nie jest spójny, ponieważ wskazuje, że wszystkie podtypy zwierząt mogą mieć jaja (w wyniku substytucji Liskova). Nastąpiłby błąd, gdyby wszystkie ssaki odpowiedziały na to żądanie „0”, ale co, jeśli włączę również metodę POST? Czy mam się obawiać, że jutro w moich naleśnikach będą jaja psa?

Jedynym sposobem na poradzenie sobie z tymi scenariuszami jest zapewnienie `` superzasobu '', który agreguje wszystkie zasoby współdzielone między wszystkimi możliwymi `` zasobami pochodnymi '', a następnie specjalizację dla każdego zasobu pochodnego, który tego potrzebuje, tak jak wtedy, gdy obniżamy obiekt do oop

GET / zwierzęta /: identyfikator zwierzęcia / synowie GET / kury /: identyfikator zwierzęcia / jaja POST / kury /: identyfikator zwierzęcia / jaja

Wadą jest to, że ktoś mógłby przekazać identyfikator psa w celu odniesienia się do instancji kolekcji kur, ale pies nie jest kurą, więc nie byłoby niepoprawne, gdyby odpowiedź wynosiła 404 lub 400 z komunikatem powodu

Czy się mylę?

Carmine Ingaldi
źródło
1
Myślę, że kładziesz zbyt duży nacisk na strukturę URI. Jedynym sposobem, w jaki powinieneś być w stanie dostać się do "animals /: animalID / eggs" jest przez HATEOAS. Więc najpierw poproś o zwierzę za pośrednictwem „animals /: animalID”, a następnie dla tych zwierząt, które mogą mieć jaja, pojawi się link do „zwierząt /: identyfikator zwierzęcia / jaja”, a dla tych, które ich nie mają, być łączem umożliwiającym przejście od zwierzęcia do jaj. Jeśli ktoś w jakiś sposób trafi do jaj zwierzęcia, które nie może mieć jaj, zwróć odpowiedni kod statusu HTTP (na przykład nie znaleziono lub zabroniony)
wired_in
0

Tak, mylisz się. Relacje można również modelować zgodnie ze specyfikacjami OpenAPI, np. W ten polimorficzny sposób.

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

...

Andreas Gaus
źródło
Dodatkowy komentarz: skupienie się na trasie API GET chicken/eggs powinno również działać przy użyciu wspólnych generatorów kodu OpenAPI dla kontrolerów, ale jeszcze tego nie sprawdzałem. Może ktoś może spróbować?
Andreas Gaus