Làm thế nào để lập mô hình một API RESTful có tính kế thừa?


87

Tôi có một hệ thống phân cấp đối tượng mà tôi cần hiển thị thông qua API RESTful và tôi không chắc các URL của mình nên được cấu trúc như thế nào và chúng sẽ trả về những gì. Tôi không thể tìm thấy bất kỳ phương pháp hay nhất nào.

Giả sử tôi có Chó và Mèo kế thừa từ Động vật. Tôi cần phẫu thuật CRUD trên chó và mèo; Tôi cũng muốn có thể thực hiện các hoạt động trên động vật nói chung.

Ý tưởng đầu tiên của tôi là làm một cái gì đó như thế này:

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

Vấn đề là bộ sưu tập / động vật bây giờ "không nhất quán", vì nó có thể quay lại và lấy các đối tượng không có cấu trúc giống hệt nhau (chó và mèo). Việc có một bộ sưu tập trả về các đối tượng có các thuộc tính khác nhau có được coi là "RESTful" không?

Một giải pháp khác là tạo một URL cho từng loại cụ thể, như sau:

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

Nhưng bây giờ tình cảm giữa chó và mèo đã mất. Nếu một người muốn truy xuất tất cả các động vật, cả tài nguyên chó và mèo phải được truy vấn. Số lượng URL cũng sẽ tăng lên với mỗi loại con động vật mới.

Một gợi ý khác là tăng cường giải pháp thứ hai bằng cách thêm điều này:

GET /animals    # get common attributes of all animals

Trong trường hợp này, các động vật được trả về sẽ chỉ chứa các thuộc tính chung cho tất cả các động vật, giảm các thuộc tính dành riêng cho chó và dành riêng cho mèo. Điều này cho phép truy xuất tất cả các động vật, mặc dù với ít chi tiết hơn. Mỗi đối tượng được trả về có thể chứa một liên kết đến phiên bản chi tiết, cụ thể.

Bất kỳ ý kiến ​​hoặc đề xuất?

Câu trả lời:


41

Tôi sẽ đề nghị:

  • Chỉ sử dụng một URI cho mỗi tài nguyên
  • Phân biệt giữa các loài động vật chỉ ở cấp thuộc tính

Thiết lập nhiều URI cho cùng một tài nguyên không bao giờ là một ý tưởng hay vì nó có thể gây ra nhầm lẫn và các tác dụng phụ không mong muốn. Do đó, URI duy nhất của bạn phải dựa trên một lược đồ chung như /animals.

Thách thức tiếp theo là xử lý toàn bộ số lượng chó và mèo ở cấp "cơ sở" đã được giải quyết nhờ /animalsphương pháp tiếp cận URI.

Thách thức cuối cùng khi xử lý các loại chuyên biệt như chó và mèo có thể dễ dàng giải quyết bằng cách sử dụng kết hợp các tham số truy vấn và thuộc tính nhận dạng trong loại phương tiện của bạn. Ví dụ:

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 - nhận tất cả chó và mèo, sẽ trả lại cả Rex và Mittens
  • GET /animals?type=dog - nhận tất cả các con chó, sẽ chỉ trả lại Rex
  • GET /animals?type=cat - nhận được tất cả mèo, sẽ chỉ Găng

Sau đó, khi tạo hoặc sửa đổi động vật, người gọi sẽ có trách nhiệm chỉ định loại động vật liên quan:

Loại phương tiện: application/vnd.vet-services.animal+json

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

Tải trọng ở trên có thể được gửi với một POSThoặc PUTyêu cầu.

Sơ đồ trên cung cấp cho bạn các đặc điểm cơ bản tương tự như thừa kế OO thông qua REST và với khả năng thêm các chuyên môn hóa khác (tức là nhiều loại động vật hơn) mà không cần phẫu thuật lớn hoặc bất kỳ thay đổi nào đối với sơ đồ URI của bạn.


Điều này có vẻ rất giống với "truyền" qua API REST. Nó cũng nhắc tôi về các vấn đề / giải pháp trong cách bố trí bộ nhớ của lớp con C ++. Ví dụ ở đâu và như thế nào để biểu diễn đồng thời cả lớp cơ sở và lớp con với một địa chỉ duy nhất trong bộ nhớ.
trcarden

10
Tôi đề nghị: GET /animals - gets all dogs and cats GET /animals/dogs - gets all dogs GET /animals/cats - gets all cats
dipold

1
Ngoài việc chỉ định kiểu mong muốn làm tham số yêu cầu GET: đối với tôi, bạn cũng có thể sử dụng kiểu accept để đạt được điều này. Đó là: GET /animals Chấp nhậnapplication/vnd.vet-services.animal.dog+json
BrianT.

22
Còn nếu mèo và chó đều có những đặc tính riêng biệt? Bạn sẽ xử lý điều đó như thế nào khi POSTvận hành, vì hầu hết các khung công tác sẽ không biết cách chuyển nó thành một mô hình đúng cách vì json không mang thông tin nhập tốt. Làm thế nào bạn sẽ xử lý các trường hợp đăng ví dụ [{"type":"dog","name":"Fido","playsFetch":true},{"type":"cat","name":"Sparkles","likesToPurr":"sometimes"}?
LB2

1
Điều gì sẽ xảy ra nếu chó và mèo có (đa số) thuộc tính khác nhau? ví dụ: # 1 ĐĂNG Thông tin liên lạc cho SMS (tới, mặt nạ) so với Email (địa chỉ email, cc, bcc, đến, từ, isHtml), hoặc ví dụ: # 2 ĐĂNG một Nguồn cấp vốn cho CreditCard (maskedPan, nameOnCard, Expiry) vs. một BankAccount (bsb, accountNumber) ... bạn vẫn sử dụng một tài nguyên API? Điều này dường như vi phạm trách nhiệm đơn lẻ từ các nguyên tắc SOLID, nhưng không chắc liệu điều này có áp dụng cho thiết kế API hay không ...
Ryan.Bartsch

5

Câu hỏi này có thể được trả lời tốt hơn với sự hỗ trợ của một cải tiến gần đây được giới thiệu trong phiên bản mới nhất của OpenAPI.

Có thể kết hợp các lược đồ bằng cách sử dụng các từ khóa như oneOf, allOf, anyOf và nhận được thông báo trọng tải được xác thực kể từ lược đồ JSON v1.0.

https://spacetelescope.github.io/undosysteming-json-schema/reference/comosystem.html

Tuy nhiên, trong OpenAPI (Swagger trước đây), thành phần lược đồ đã được tăng cường bởi các từ khóa phân biệt (v2.0 +) và oneOf (v3.0 +) để thực sự hỗ trợ tính đa hình.

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

Kế thừa của bạn có thể được mô hình hóa bằng cách sử dụng kết hợp oneOf (để chọn một trong các kiểu con) và allOf (để kết hợp kiểu và một trong các kiểu con của nó). Dưới đây là định nghĩa mẫu cho phương thức 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
Đúng là OAS cho phép điều này. Tuy nhiên, không có hỗ trợ nào cho tính năng được hiển thị trong Swagger UI ( liên kết ) và tôi nghĩ rằng một tính năng sẽ được sử dụng hạn chế nếu bạn không thể hiển thị nó cho bất kỳ ai.
emft

1
@emft, không đúng. Khi viết câu trả lời này, Swagger UI đã hỗ trợ điều đó.
Andrejs Cainikovs

Cảm ơn, điều này làm việc tuyệt vời! Có vẻ như hiện tại, giao diện người dùng Swagger không hiển thị đầy đủ điều này. Mô hình sẽ hiển thị trong phần Giản đồ ở dưới cùng và bất kỳ phần phản hồi nào tham chiếu đến phần oneOf sẽ hiển thị một phần trong giao diện người dùng (chỉ lược đồ, không có ví dụ), nhưng bạn không nhận được nội dung mẫu nào cho đầu vào yêu cầu. Vấn đề github cho vấn đề này đã mở được 3 năm nên có khả năng vẫn như vậy: github.com/swagger-api/swagger-ui/issues/3803
Jethro

4

Tôi sẽ cho / động vật trả lại danh sách cả chó và cá và những thứ khác:

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

Sẽ dễ dàng triển khai một ví dụ JSON tương tự.

Khách hàng luôn có thể dựa vào yếu tố "tên" ở đó (một thuộc tính chung). Nhưng tùy thuộc vào thuộc tính "type" sẽ có các phần tử khác như một phần của biểu diễn động vật.

Vốn dĩ không có gì RESTful hoặc unRESTful khi trả về một danh sách như vậy - REST không quy định bất kỳ định dạng cụ thể nào để biểu diễn dữ liệu. Tất cả những gì nó nói là dữ liệu phải có một số biểu diễn và định dạng cho biểu diễn đó được xác định bởi loại phương tiện (mà trong HTTP là tiêu đề Loại-Nội dung).

Hãy nghĩ về các trường hợp sử dụng của bạn - bạn có cần hiển thị danh sách các động vật hỗn hợp không? Vâng, sau đó trả về danh sách dữ liệu động vật hỗn hợp. Bạn có cần một danh sách chỉ những con chó? Chà, hãy lập một danh sách như vậy.

Cho dù bạn làm / thú vật? Type = dog hay / dog đều không liên quan đến REST không quy định bất kỳ định dạng URL nào - được để lại dưới dạng chi tiết triển khai bên ngoài phạm vi của REST. REST chỉ nói rằng các tài nguyên phải có số nhận dạng - đừng bận tâm đến định dạng nào.

Bạn nên thêm một số liên kết siêu phương tiện để đến gần hơn với API RESTful. Ví dụ: bằng cách thêm tham chiếu đến các chi tiết động vậ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>

Bằng cách thêm liên kết siêu phương tiện, bạn giảm sự ghép nối giữa máy khách / máy chủ - trong trường hợp trên, bạn trút bỏ gánh nặng xây dựng URL khỏi máy khách và để máy chủ quyết định cách tạo URL (theo định nghĩa là cơ quan duy nhất của nó).


1

Nhưng bây giờ tình cảm giữa chó và mèo đã mất.

Thật vậy, nhưng hãy nhớ rằng URI chỉ đơn giản là không bao giờ phản ánh mối quan hệ giữa các đối tượng.


1

Tôi biết đây là một câu hỏi cũ, nhưng tôi muốn điều tra các vấn đề khác về mô hình kế thừa RESTful

Tôi luôn có thể nói rằng con chó là một con vật và con gà mái cũng vậy, nhưng con gà mái tạo ra trứng trong khi con chó là động vật có vú, vì vậy nó không thể. Một API như

NHẬN động vật /: animalID / trứng

không nhất quán vì chỉ ra rằng tất cả các kiểu phụ của động vật đều có thể có trứng (do sự thay thế Liskov). Sẽ có một dự phòng nếu tất cả các loài động vật có vú trả lời bằng '0' cho yêu cầu này, nhưng nếu tôi cũng bật phương thức POST thì sao? Tôi có nên sợ rằng ngày mai sẽ có trứng chó trong bánh crepe của tôi không?

Cách duy nhất để xử lý các tình huống này là cung cấp một 'siêu tài nguyên' tổng hợp tất cả các nguồn phụ được chia sẻ giữa tất cả các 'tài nguyên có nguồn gốc' có thể di động và sau đó là chuyên môn hóa cho từng tài nguyên dẫn xuất cần nó, giống như khi chúng ta downcast một đối tượng vào oop

GET / animal /: animalID / con trai GET / hens /: animalID / egg POST / hens /: animalID / egg

Hạn chế ở đây là ai đó có thể chuyển Id con chó để tham chiếu một ví dụ về bộ sưu tập gà mái, nhưng con chó không phải là gà mái, vì vậy sẽ không sai nếu câu trả lời là 404 hoặc 400 kèm theo thông báo lý do.

Tôi có lầm không?


1
Tôi nghĩ rằng bạn đang đặt nặng quá nhiều vào cấu trúc URI. Cách duy nhất bạn có thể truy cập "động vật /: thú vật / trứng" là thông qua HATEOAS. Vì vậy, trước tiên bạn sẽ yêu cầu động vật thông qua "animal /: animalID", sau đó đối với những động vật có thể có trứng, sẽ có một liên kết đến "động vật /: thú vật / trứng" và đối với những động vật không có, sẽ không là một liên kết để truyền từ động vật sang trứng. Nếu ai đó bằng cách nào đó kết thúc trứng cho một con vật không thể có trứng, hãy trả lại mã trạng thái HTTP thích hợp (ví dụ: không tìm thấy hoặc bị cấm)
wired_in

0

Vâng, bạn đã sai. Ngoài ra, các mối quan hệ có thể được mô hình hóa theo các thông số kỹ thuật của OpenAPI, ví dụ theo cách đa hình này.

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

...


Nhận xét thêm: tập trung vào tuyến API GET chicken/eggs cũng sẽ hoạt động bằng cách sử dụng các trình tạo mã OpenAPI phổ biến cho bộ điều khiển, nhưng tôi chưa kiểm tra điều này. Có lẽ ai đó có thể thử?
Andreas Gaus
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.