Bagaimana cara memodelkan RESTful API dengan warisan?

88

Saya memiliki hierarki objek yang perlu saya ungkapkan melalui RESTful API dan saya tidak yakin bagaimana URL saya harus terstruktur dan apa yang harus dikembalikan. Saya tidak dapat menemukan praktik terbaik apa pun.

Katakanlah saya memiliki Anjing dan Kucing yang diwarisi dari Hewan. Saya perlu operasi CRUD pada anjing dan kucing; Saya juga ingin bisa melakukan operasi pada hewan secara umum.

Ide pertama saya adalah melakukan sesuatu seperti ini:

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

Masalahnya, koleksi / animals sekarang "tidak konsisten", karena koleksi dapat kembali dan mengambil objek yang tidak memiliki struktur yang persis sama (anjing dan kucing). Apakah dianggap "RESTful" memiliki koleksi yang menampilkan objek yang memiliki atribut berbeda?

Solusi lain adalah membuat URL untuk setiap jenis konkret, seperti ini:

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

Namun kini hubungan antara anjing dan kucing terputus. Jika seseorang ingin mengambil semua hewan, sumber daya anjing dan kucing harus dipertanyakan. Jumlah URL juga akan meningkat dengan setiap subtipe hewan baru.

Saran lain adalah menambah solusi kedua dengan menambahkan ini:

GET /animals    # get common attributes of all animals

Dalam kasus ini, hewan yang dikembalikan hanya akan berisi atribut yang umum untuk semua hewan, menghilangkan atribut khusus anjing dan kucing. Ini memungkinkan untuk mengambil semua hewan, meskipun dengan detail yang lebih sedikit. Setiap objek yang dikembalikan dapat berisi tautan ke versi konkret yang mendetail.

Ada komentar atau saran?

Alpha Hydrae
sumber

Jawaban:

41

Saya akan menyarankan:

  • Hanya menggunakan satu URI per resource
  • Membedakan hewan hanya pada tingkat atribut

Menyiapkan beberapa URI ke sumber yang sama bukanlah ide yang baik karena dapat menyebabkan kebingungan dan efek samping yang tidak terduga. Mengingat itu, URI tunggal Anda harus didasarkan pada skema umum seperti /animals.

Tantangan berikutnya dalam menangani seluruh koleksi anjing dan kucing di tingkat "dasar" telah diselesaikan dengan /animalspendekatan URI.

Tantangan terakhir dalam menangani jenis khusus seperti anjing dan kucing dapat dengan mudah diselesaikan menggunakan kombinasi parameter kueri dan atribut identifikasi dalam jenis media Anda. Sebagai contoh:

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 - mendapatkan semua anjing dan kucing, akan mengembalikan Rex dan Mittens
  • GET /animals?type=dog - mendapatkan semua anjing, hanya akan mengembalikan Rex
  • GET /animals?type=cat - mendapatkan semua kucing, hanya akan Mittens

Kemudian saat membuat atau memodifikasi hewan, pemanggil wajib menentukan jenis hewan yang terlibat:

Tipe media: application/vnd.vet-services.animal+json

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

Muatan di atas dapat dikirim dengan permintaan POSTatau PUT.

Skema di atas memberi Anda karakteristik dasar yang serupa seperti warisan OO melalui REST, dan dengan kemampuan untuk menambahkan spesialisasi lebih lanjut (yaitu lebih banyak jenis hewan) tanpa operasi besar atau perubahan apa pun pada skema URI Anda.

Brian Kelly
sumber
Ini sepertinya sangat mirip dengan "casting" melalui REST API. Ini juga mengingatkan saya pada masalah / solusi dalam tata letak memori subkelas C ++. Misalnya di mana dan bagaimana merepresentasikan secara bersamaan basis dan subkelas dengan satu alamat di memori.
trcarden
10
Saya sarankan: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
dipold
1
Selain menentukan jenis yang diinginkan sebagai parameter permintaan GET: menurut saya, Anda dapat menggunakan jenis terima untuk mencapai ini juga. Yaitu: GET /animals Terimaapplication/vnd.vet-services.animal.dog+json
BrianT.
22
Bagaimana jika kucing dan anjing masing-masing memiliki sifat yang unik? Bagaimana Anda menanganinya saat POSTberoperasi, karena sebagian besar kerangka kerja tidak akan tahu cara mendesentralisasikannya dengan benar ke dalam model karena json tidak membawa info pengetikan yang baik. Bagaimana Anda menangani kasus posting misalnya [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
LB2
1
Bagaimana jika anjing dan kucing memiliki (mayoritas) sifat yang berbeda? misalnya # 1 MEMASANG Komunikasi untuk SMS (ke, mask) vs. Email (alamat email, cc, bcc, ke, dari, isHtml), atau misalnya # 2 MEMASANG Sumber Dana untuk Kartu Kredit (maskedPan, nameOnCard, Kedaluwarsa) vs. a BankAccount (bsb, accountNumber) ... apakah Anda masih akan menggunakan satu sumber API? Ini tampaknya melanggar tanggung jawab tunggal dari prinsip SOLID, tetapi tidak yakin apakah ini berlaku untuk desain API ...
Ryan.Bartsch
5

Pertanyaan ini dapat dijawab dengan lebih baik dengan dukungan dari peningkatan terbaru yang diperkenalkan di versi terbaru OpenAPI.

Skema dapat digabungkan menggunakan kata kunci seperti oneOf, allOf, anyOf, dan mendapatkan payload pesan yang divalidasi sejak skema JSON v1.0.

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

Namun, di OpenAPI (sebelumnya Swagger), komposisi skema telah ditingkatkan dengan kata kunci diskriminator (v2.0 +) dan oneOf (v3.0 +) untuk benar-benar mendukung polimorfisme.

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

Pewarisan Anda dapat dimodelkan menggunakan kombinasi oneOf (untuk memilih salah satu subtipe) dan allOf (untuk menggabungkan tipe dan salah satu subtipe). Di bawah ini adalah contoh definisi untuk metode 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.dll
sumber
1
Memang benar OAS mengizinkan ini. Namun, tidak ada dukungan untuk fitur yang akan ditampilkan di Swagger UI ( tautan ), dan menurut saya fitur memiliki penggunaan terbatas jika Anda tidak dapat menampilkannya kepada siapa pun.
emft
1
@ Emm, tidak benar. Saat menulis jawaban ini, Swagger UI sudah mendukungnya.
Andrejs Cainikovs
Terima kasih, ini berfungsi dengan baik! Tampaknya saat ini benar bahwa UI Swagger tidak menampilkan ini sepenuhnya. Model akan ditampilkan di bagian Skema di bagian bawah, dan setiap bagian respons yang mereferensikan bagian oneOf akan ditampilkan sebagian di UI (hanya skema, tidak ada contoh), tetapi Anda tidak mendapatkan isi contoh untuk input permintaan. Masalah github untuk ini telah terbuka selama 3 tahun sehingga kemungkinan besar akan tetap seperti itu: github.com/swagger-api/swagger-ui/issues/3803
Jethro
4

Saya akan pergi untuk / animals mengembalikan daftar anjing dan ikan dan apa lagi:

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

Harus mudah untuk menerapkan contoh JSON yang serupa.

Klien selalu dapat mengandalkan elemen "nama" yang ada (atribut umum). Tetapi bergantung pada atribut "type", akan ada elemen lain sebagai bagian dari representasi hewan.

Tidak ada yang secara inheren RESTful atau unRESTful dalam mengembalikan daftar seperti itu - REST tidak menentukan format spesifik apa pun untuk merepresentasikan data. Semua yang dikatakannya adalah bahwa data harus memiliki beberapa representasi dan format untuk representasi itu diidentifikasi oleh jenis media (yang dalam HTTP adalah header Jenis Konten).

Pikirkan tentang kasus penggunaan Anda - apakah Anda perlu menunjukkan daftar hewan campuran? Nah, kemudian kembalikan daftar data hewan campuran. Apakah Anda hanya memerlukan daftar anjing? Nah, buatlah daftar seperti itu.

Apakah Anda melakukan / animals? Type = dog atau / dogs tidak relevan sehubungan dengan REST yang tidak menentukan format URL apa pun - yang dibiarkan sebagai detail penerapan di luar cakupan REST. REST hanya menyatakan bahwa resource harus memiliki pengenal - apalagi formatnya.

Anda harus menambahkan beberapa hyper media linking agar lebih dekat dengan RESTful API. Misalnya dengan menambahkan referensi ke detail hewan:

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

Dengan menambahkan hyper media linking Anda mengurangi penggandengan klien / server - dalam kasus di atas, Anda mengambil beban konstruksi URL dari klien dan membiarkan server memutuskan bagaimana membuat URL (yang menurut definisi adalah satu-satunya otoritas).

Jørn Wildt
sumber
1

Namun kini hubungan antara anjing dan kucing terputus.

Memang, namun perlu diingat bahwa URI tidak pernah mencerminkan hubungan antar objek.

G. Demecki
sumber
1

Saya tahu ini adalah pertanyaan lama, tetapi saya tertarik untuk menyelidiki masalah lebih lanjut tentang pemodelan warisan RESTful

Saya selalu dapat mengatakan bahwa anjing adalah binatang dan ayam juga, tetapi ayam membuat telur sedangkan anjing adalah mamalia, jadi tidak bisa. Seperti API

DAPATKAN hewan /: animalID / telur

tidak konsisten karena menunjukkan bahwa semua subtipe hewan dapat bertelur (sebagai akibat dari substitusi Liskov). Akan ada kemunduran jika semua mamalia merespons dengan '0' untuk permintaan ini, tetapi bagaimana jika saya juga mengaktifkan metode POST? Haruskah saya takut besok akan ada telur anjing di crepes saya?

Satu-satunya cara untuk menangani skenario ini adalah dengan menyediakan 'sumber daya super' yang menggabungkan semua sub-sumber daya yang dibagi di antara semua 'sumber daya turunan' yang memungkinkan dan kemudian spesialisasi untuk setiap sumber daya turunan yang membutuhkannya, seperti saat kita men-downcast sebuah objek menjadi oop

GET / animals /: animalID / sons GET / hens /: animalID / egg POST / hens /: animalID / telor

Kekurangannya, di sini, seseorang dapat memberikan ID anjing untuk referensi contoh koleksi ayam, tetapi anjing tersebut bukan ayam, jadi tidak salah jika responnya adalah 404 atau 400 dengan pesan alasan

Apakah aku salah?

Carmine Ingaldi
sumber
1
Saya pikir Anda terlalu menekankan pada struktur URI. Satu-satunya cara agar Anda bisa mendapatkan "animals /: animalID / eggs" adalah melalui HATEOAS. Jadi, pertama-tama Anda akan meminta hewan melalui "animals /: animalID" lalu untuk hewan yang dapat memiliki telur, akan ada tautan ke "animals /: animalID / telur", dan bagi yang tidak, tidak akan menjadi penghubung untuk berpindah dari hewan ke telur. Jika seseorang entah bagaimana berakhir dengan telur untuk hewan yang tidak dapat memiliki telur, kembalikan kode status HTTP yang sesuai (tidak ditemukan atau dilarang misalnya)
wired_in
0

Ya kamu salah Juga hubungan dapat dimodelkan mengikuti spesifikasi OpenAPI misalnya dengan cara polimorfik ini.

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
sumber
Komentar tambahan: memfokuskan rute API GET chicken/eggs juga harus bekerja menggunakan generator kode OpenAPI umum untuk pengontrol, tetapi saya belum memeriksa ini. Mungkin seseorang bisa mencoba?
Andreas Gaus