จะสร้างโมเดล RESTful API ด้วยการสืบทอดได้อย่างไร


88

ฉันมีลำดับชั้นของออบเจ็กต์ที่ต้องแสดงผ่าน RESTful API และฉันไม่แน่ใจว่า URL ของฉันควรมีโครงสร้างอย่างไรและควรส่งคืนอะไร ฉันไม่พบแนวทางปฏิบัติที่ดีที่สุด

สมมติว่าฉันมีสุนัขและแมวที่สืบทอดมาจากสัตว์ ฉันต้องการการผ่าตัด CRUD กับสุนัขและแมว ฉันยังต้องการที่จะดำเนินการกับสัตว์โดยทั่วไป

ความคิดแรกของฉันคือการทำสิ่งนี้:

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

สิ่งนี้คือตอนนี้คอลเลกชัน / สัตว์ "ไม่สอดคล้องกัน" เนื่องจากสามารถส่งคืนและรับวัตถุที่ไม่มีโครงสร้างเหมือนกัน (สุนัขและแมว) ถือว่า "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 - รับสุนัขและแมวทั้งหมดจะคืนทั้ง Rex และ Mittens
  • GET /animals?type=dog - รับสุนัขทุกตัวจะคืนเร็กซ์เท่านั้น
  • GET /animals?type=cat - รับแมวทั้งหมดจะมีเพียง Mittens

จากนั้นเมื่อสร้างหรือปรับเปลี่ยนสัตว์ผู้เรียกใช้จะต้องระบุประเภทของสัตว์ที่เกี่ยวข้อง:

ประเภทสื่อ: application/vnd.vet-services.animal+json

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

สามารถส่ง payload ข้างต้นพร้อมกับ a POSTหรือPUTคำขอ

รูปแบบข้างต้นทำให้คุณมีลักษณะพื้นฐานที่คล้ายคลึงกับการสืบทอด OO ผ่าน 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 สำหรับฉันแล้วคุณสามารถใช้ประเภทยอมรับเพื่อให้บรรลุสิ่งนี้ได้เช่นกัน นั่นคือ: 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 (ถึง, มาสก์) กับอีเมล (ที่อยู่อีเมล, cc, bcc, ถึง, จาก, isHtml) หรือเช่น # 2 การโพสต์แหล่งที่มาของ FundingSource สำหรับ CreditCard (maskedPan, nameOnCard, Expiry) เทียบกับ BankAccount (bsb, accountNumber) ... คุณจะยังใช้ทรัพยากร API เดียวหรือไม่ สิ่งนี้ดูเหมือนจะละเมิดความรับผิดชอบเดียวจากหลักการ SOLID แต่ไม่แน่ใจว่าสิ่งนี้ใช้ได้กับการออกแบบ API หรือไม่ ...
Ryan.Bartsch

5

คำถามนี้สามารถตอบได้ดีขึ้นด้วยการสนับสนุนการปรับปรุงล่าสุดที่เปิดตัวใน OpenAPI เวอร์ชันล่าสุด

เป็นไปได้ที่จะรวมสคีมาโดยใช้คีย์เวิร์ดเช่น oneOf, allOf, anyOf และรับน้ำหนักบรรทุกข้อความที่ตรวจสอบตั้งแต่ JSON schema v1.0

https://spacetelescope.github.io/understand-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

1
เป็นเรื่องจริงที่ OAS อนุญาต อย่างไรก็ตามไม่มีการสนับสนุนฟีเจอร์ที่จะแสดงใน Swagger UI ( ลิงก์ ) และฉันคิดว่าฟีเจอร์นี้มีการใช้งานที่ จำกัด หากคุณไม่สามารถแสดงให้ใครเห็นได้
emft

1
@emft ไม่จริง. ในขณะที่เขียนคำตอบนี้ Swagger UI รองรับอยู่แล้ว
Andrejs Cainikovs

ขอบคุณมันใช้งานได้ดีมาก! ดูเหมือนว่าในขณะนี้ UI ของ Swagger จะไม่แสดงสิ่งนี้อย่างเต็มที่ โมเดลจะแสดงในส่วนสคีมาที่ด้านล่างและส่วนการตอบสนองใด ๆ ที่อ้างถึงส่วน oneOf จะแสดงบางส่วนใน UI (สคีมาเท่านั้นไม่มีตัวอย่าง) แต่คุณไม่ได้รับเนื้อหาตัวอย่างสำหรับอินพุตคำขอ ปัญหา 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 ที่คล้ายกันได้ง่าย

ไคลเอ็นต์สามารถพึ่งพาองค์ประกอบ "ชื่อ" ที่มีอยู่ได้ตลอดเวลา (แอตทริบิวต์ทั่วไป) แต่ขึ้นอยู่กับแอตทริบิวต์ "ประเภท" จะมีองค์ประกอบอื่น ๆ เป็นส่วนหนึ่งของการแสดงสัตว์

ไม่มีสิ่งใดที่จะคืนค่าหรือไม่ได้รับผลกระทบโดยเนื้อแท้ในการส่งคืนรายการดังกล่าว - REST ไม่ได้กำหนดรูปแบบเฉพาะสำหรับการแสดงข้อมูล ทั้งหมดก็บอกว่าเป็นข้อมูลที่จะต้องมีบางอย่างที่เป็นตัวแทนและรูปแบบสำหรับการแสดงที่มีการระบุชนิดของสื่อ (ซึ่งใน HTTP เป็นส่วนหัว Content-Type)

ลองนึกถึงกรณีการใช้งานของคุณ - คุณต้องแสดงรายการสัตว์ผสมหรือไม่? จากนั้นส่งคืนรายการข้อมูลสัตว์ผสม คุณต้องการรายชื่อสุนัขเท่านั้นหรือไม่? ทำรายการดังกล่าว

ไม่ว่าคุณจะทำ / animals? 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

ฉันรู้ว่านี่เป็นคำถามเก่า แต่ฉันสนใจที่จะตรวจสอบปัญหาเพิ่มเติมเกี่ยวกับการสร้างแบบจำลองการสืบทอดที่ไม่เหมาะสม

ฉันสามารถพูดได้เสมอว่าสุนัขก็เป็นสัตว์และแม่ไก่เช่นกัน แต่แม่ไก่ก็ออกไข่ในขณะที่สุนัขเป็นสัตว์เลี้ยงลูกด้วยนมดังนั้นจึงทำไม่ได้ API เช่น

รับสัตว์ /: AnimalID / ไข่

ไม่สอดคล้องกันเนื่องจากบ่งชี้ว่าสัตว์ทุกชนิดสามารถมีไข่ได้ (อันเป็นผลมาจากการทดแทน Liskov) จะมีทางเลือกอื่นหากสัตว์เลี้ยงลูกด้วยนมทั้งหมดตอบสนองด้วย '0' ต่อคำขอนี้ แต่ถ้าฉันเปิดใช้งานวิธี POST ด้วยล่ะ ฉันจะกลัวไหมว่าพรุ่งนี้จะมีไข่สุนัขในเครปของฉัน?

วิธีเดียวที่จะจัดการกับสถานการณ์เหล่านี้คือการจัดเตรียม 'super-resource' ซึ่งรวบรวมทรัพยากรย่อยทั้งหมดที่ใช้ร่วมกันระหว่าง 'ทรัพยากรที่ได้รับ' ที่เป็นไปได้ทั้งหมดจากนั้นจึงมีความเชี่ยวชาญเฉพาะสำหรับทรัพยากรที่ได้รับแต่ละรายการที่ต้องการเช่นเดียวกับเมื่อเราดาวน์คาสต์วัตถุ ในโอ๊ะโอ

รับ / สัตว์ /: สัตว์ ID / ลูกชาย GET / แม่ไก่ /: AnimalID / ไข่ POST / แม่ไก่ /: AnimalID / ไข่

ข้อเสียเปรียบตรงนี้คือบางคนสามารถส่งรหัสสุนัขเพื่ออ้างอิงตัวอย่างของการรวบรวมแม่ไก่ได้ แต่สุนัขไม่ใช่แม่ไก่ดังนั้นจึงไม่ผิดหากคำตอบคือ 404 หรือ 400 พร้อมข้อความเหตุผล

ฉันผิดเหรอ?


1
ฉันคิดว่าคุณให้ความสำคัญกับโครงสร้าง URI มากเกินไป วิธีเดียวที่คุณจะสามารถเข้าถึง "สัตว์ /: animalID / ไข่" ได้คือผ่าน HATEOAS ดังนั้นคุณจึงขอสัตว์ผ่านทาง "animals /: animalID" ก่อนจากนั้นสำหรับสัตว์เหล่านั้นที่สามารถมีไข่ได้จะมีลิงก์ไปที่ "สัตว์ /: animalID / ไข่" และสำหรับสัตว์ที่ไม่มีจะไม่มี เป็นลิงค์ที่จะได้รับจากสัตว์สู่ไข่ หากมีใครไปพบไข่ของสัตว์ที่ไม่สามารถมีไข่ได้ให้ส่งคืนรหัสสถานะ HTTP ที่เหมาะสม (ไม่พบหรือไม่ได้รับอนุญาตเป็นต้น)
สาย _ ใน

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 ทั่วไปสำหรับคอนโทรลเลอร์ แต่ฉันยังไม่ได้ตรวจสอบสิ่งนี้ อาจจะมีคนลอง?
Andreas Gaus
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.