Bagaimana cara merancang REST API yang kompleks dengan mempertimbangkan kinerja DB?


8

Saya telah mengikuti beberapa tutorial tentang cara mendesain API REST, tetapi saya masih memiliki beberapa tanda tanya besar. Semua tutorial ini menunjukkan sumber daya dengan hierarki yang relatif sederhana, dan saya ingin tahu bagaimana prinsip-prinsip yang digunakan di dalamnya berlaku untuk yang lebih kompleks. Selain itu, mereka tinggal di tingkat arsitektur yang sangat tinggi. Mereka nyaris tidak menunjukkan kode yang relevan, apalagi lapisan kegigihan. Saya secara khusus prihatin dengan beban / kinerja basis data, seperti yang dikatakan Gavin King :

Anda akan menghemat usaha jika Anda memperhatikan database di semua tahap pengembangan

Katakanlah aplikasi saya akan memberikan pelatihan untuk Companies. Companiesmiliki Departmentsdan Offices. Departmentsmiliki Employees. Employeesmiliki Skillsdan Courses, dan Levelketerampilan tertentu diperlukan untuk dapat mendaftar untuk beberapa kursus. Hirarki adalah sebagai berikut, tetapi dengan:

-Companies
  -Departments
    -Employees
      -PersonalInformation
        -Address
      -Skills (quasi-static data)
        -Levels (quasi-static data)
      -Courses
        -Address
  -Offices
    -Address

Paths akan menjadi sesuatu seperti:

companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1

Mengambil sumber daya

Jadi ok, ketika mengembalikan perusahaan, saya jelas tidak mengembalikan seluruh hirarki companies/1/departments/1/employees/1/courses/1+ companies/1/offices/../. Saya mungkin mengembalikan daftar tautan ke departemen atau departemen yang diperluas, dan harus mengambil keputusan yang sama di tingkat ini: apakah saya mengembalikan daftar tautan ke karyawan departemen atau karyawan yang diperluas? Itu akan tergantung pada jumlah departemen, karyawan, dll.

Pertanyaan 1 : Apakah pemikiran saya benar, apakah "tempat untuk memotong hierarki" adalah keputusan rekayasa yang harus saya buat?

Sekarang katakanlah ketika ditanya GET companies/id, saya memutuskan untuk mengembalikan daftar tautan ke koleksi departemen, dan informasi kantor yang diperluas. Perusahaan saya tidak memiliki banyak kantor, jadi bergabunglah dengan tabel Officesdan Addressesseharusnya tidak menjadi masalah besar. Contoh tanggapan:

GET /companies/1

200 OK
{
  "_links":{
    "self" : {
      "href":"http://trainingprovider.com:8080/companies/1"
      },
      "offices": [
            { "href": "http://trainingprovider.com:8080/companies/1/offices/1"},
            { "href": "http://trainingprovider.com:8080/companies/1/offices/2"},
            { "href": "http://trainingprovider.com:8080/companies/1/offices/3"}
      ],
      "departments": [
            { "href": "http://trainingprovider.com:8080/companies/1/departments/1"},
            { "href": "http://trainingprovider.com:8080/companies/1/departments/2"},
            { "href": "http://trainingprovider.com:8080/companies/1/departments/3"}
      ]
  }
  "name":"Acme",
  "industry":"Manufacturing",
  "description":"Some text here",
  "offices": {
    "_meta":{
      "href":"http://trainingprovider.com:8080/companies/1/offices"
      // expanded offices information here
    }
  }
}

Pada level kode, ini menyiratkan bahwa (menggunakan Hibernate, saya tidak yakin bagaimana dengan penyedia lain, tapi saya kira itu hampir sama) Saya tidak akan menempatkan koleksi Departmentsebagai bidang di Companykelas saya , karena:

  • Seperti yang saya katakan, saya tidak memuatnya Company, jadi saya tidak ingin memuatnya dengan penuh semangat
  • Dan jika saya tidak memuatnya dengan penuh semangat, saya mungkin juga menghapusnya, karena konteks kegigihan akan ditutup setelah saya memuat Perusahaan dan tidak ada gunanya mencoba memuatnya setelah itu ( LazyInitializationException).

Kemudian, saya akan menempatkan Integer companyIddi Departmentkelas, sehingga saya dapat menambahkan departemen ke perusahaan.

Juga, saya perlu mendapatkan id dari semua departemen. Hit lain ke DB tapi bukan yang berat, jadi harusnya ok. Kode tersebut dapat terlihat seperti:

@Service
@Path("/companies")
public class CompanyResource {

    @Autowired
    private CompanyService companyService;

    @Autowired
    private CompanyParser companyParser;

    @Path("/{id}")
    @GET
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response findById(@PathParam("id") Integer id) {
        Optional<Company> company = companyService.findById(id);
        if (!company.isPresent()) {
            throw new CompanyNotFoundException();
        }
        CompanyResponse companyResponse = companyParser.parse(company.get());
        // Creates a DTO with a similar structure to Company, and recursivelly builds
        // sub-resource DTOs such as OfficeDTO
        Set<Integer> departmentIds = companyService.getDepartmentIds(id);
        // "SELECT id FROM departments WHERE companyId = id"
        // add list of links to the response
        return Response.ok(companyResponse).build();
    }
}
@Entity
@Table(name = "companies")
public class Company {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String industry;

    @OneToMany(fetch = EAGER, cascade = {ALL}, orphanRemoval = true)
    @JoinColumn(name = "companyId_fk", referencedColumnName = "id", nullable = false)
    private Set<Office> offices = new HashSet<>();

    // getters and setters
}
@Entity
@Table(name = "departments")
public class Department {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private Integer companyId;

    @OneToMany(fetch = EAGER, cascade = {ALL}, orphanRemoval = true)
    @JoinColumn(name = "departmentId", referencedColumnName = "id", nullable = false)
    private Set<Employee> employees = new HashSet<>();

    // getters and setters
}

Memperbarui sumber daya

Untuk operasi pembaruan, saya dapat mengekspos titik akhir dengan PUTatau POST. Karena saya ingin saya PUTmenjadi idempoten, saya tidak dapat mengizinkan pembaruan parsial . Tetapi kemudian, jika saya ingin memodifikasi bidang deskripsi perusahaan, saya perlu mengirim seluruh perwakilan sumber daya. Itu sepertinya terlalu membengkak. Hal yang sama ketika memperbarui karyawan PersonalInformation. Saya tidak berpikir masuk akal harus mengirim semua Skills+ Coursesbersama-sama dengan itu.

Pertanyaan 2 : Apakah PUT hanya digunakan untuk sumber daya halus?

Saya telah melihat di dalam log bahwa, ketika menggabungkan suatu entitas, Hibernate mengeksekusi sekelompok SELECTquery. Saya kira itu hanya untuk memeriksa apakah ada yang berubah dan memperbarui informasi apa pun yang diperlukan. Semakin tinggi entitas dalam hierarki, semakin berat dan semakin kompleks kueri. Tetapi beberapa sumber menyarankan untuk menggunakan sumber daya berbutir kasar . Jadi sekali lagi, saya perlu memeriksa berapa banyak tabel yang terlalu banyak, dan menemukan kompromi antara granularity sumber daya dan kompleksitas permintaan DB.

Pertanyaan 3 : Apakah ini sekadar keputusan teknis "tahu di mana harus memotong" atau apakah saya kehilangan sesuatu?

Pertanyaan 4 : Apakah ini, atau jika tidak, apa "proses berpikir" yang tepat ketika merancang layanan REST dan mencari kompromi antara granularitas sumber daya, kompleksitas kueri, dan kedekatan jaringan?


1
1. Ya; karena panggilan REST mahal, penting untuk mencoba dan mendapatkan rincian yang tepat.
Robert Harvey

1
2. Tidak. Kata kerja PUT tidak ada hubungannya dengan rincian, per se.
Robert Harvey

1
3. Ya. Tidak, Anda tidak melewatkan apa pun.
Robert Harvey

1
4. Pemikiran yang tepat adalah "melakukan apa yang paling memenuhi persyaratan Anda untuk skalabilitas, kinerja, pemeliharaan dan masalah lainnya." Ini mungkin memerlukan beberapa percobaan untuk menemukan sweet spot.
Robert Harvey

4
Terlalu panjang. Tidak membaca Bisakah ini diludahi menjadi 4 pertanyaan aktual?
MetaFight

Jawaban:


7

Saya pikir Anda memiliki kompleksitas karena Anda mulai dengan komplikasi berlebihan:

Paths akan menjadi sesuatu seperti:

companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1

Alih-alih, saya akan memperkenalkan skema URL yang lebih sederhana seperti ini:

GET companies/
    Returns a list of companies, for each company 
    return short essential info (ID, name, maybe industry)
GET companies/1
    Returns single company info like this:

    {
        "name":"Acme",
        "description":"Some text here"
        "industry":"Manufacturing"
        departments: {
            "href":"/companies/1/departments"
            "count": 5
        }
        offices: {
            "href":"/companies/1/offices"
            "count": 3
        }
    }

    We don't expand the data for internal sub-resources, 
    just return the count, so client knows that some data is present.
    In some cases count may be not needed too.
GET companies/1/departments
    Returns company departments, again short info for each department
GET departments/
    Here you need to decide if it makes sense to expose 
    a list of departments or not. 
    If not - leave only companies/X/departments method.

    Note, that you can also use query string to make this 
    method "searchable", like:
        /departments?company=1 - list of all departments for company 1
        /departments?type=support - all 'support' departments for all companies
GET departments/1
    Returns department 1 data

Dengan cara ini menjawab sebagian besar pertanyaan Anda - Anda "memotong" hierarki segera dan Anda tidak mengikat skema URL Anda ke struktur data internal. Misalnya, jika kita mengetahui ID karyawan, apakah Anda berharap akan menanyakannya suka employees/:IDatau suka companies/:X/departments/:Y/employees/:ID?

Mengenai PUTvs POSTpermintaan, dari pertanyaan Anda jelas bahwa Anda merasa pembaruan parsial akan lebih efisien untuk data Anda. Jadi saya hanya akan menggunakan POSTs.

Dalam praktiknya, Anda sebenarnya ingin menyimpan data ( GETpermintaan) cache dan kurang penting untuk pembaruan data. Dan pembaruan seringkali tidak dapat di-cache terlepas dari jenis permintaan apa yang Anda lakukan (seperti jika server secara otomatis menetapkan waktu pembaruan - itu akan berbeda untuk setiap permintaan).

Pembaruan: tentang "proses berpikir" yang tepat - karena didasarkan pada HTTP, kita dapat menerapkan cara berpikir yang teratur saat merancang struktur situs web. Dalam hal ini di atas kita dapat memiliki daftar perusahaan dan menunjukkan deskripsi singkat untuk masing-masing dengan tautan ke halaman "lihat perusahaan", di mana kita menunjukkan rincian perusahaan dan tautan ke kantor / departemen dan sebagainya.


5

IMHO, saya pikir Anda tidak mengerti intinya.

Pertama, kinerja REST API dan DB tidak terkait .

API REST hanyalah sebuah antarmuka , itu tidak mendefinisikan sama sekali bagaimana Anda melakukan hal-hal di bawah tenda. Anda dapat memetakannya ke struktur DB yang Anda suka di belakangnya. Karena itu:

  1. desain API Anda sehingga mudah bagi pengguna
  2. desain basis data Anda sehingga cukup untuk mengukur:
    • pastikan Anda memiliki indeks yang tepat
    • jika Anda menyimpan benda, pastikan saja tidak terlalu besar.

Itu dia.

... dan terakhir, baunya seperti optimasi prematur. Tetap sederhana, cobalah, dan sesuaikan jika perlu.


2

Pertanyaan 1: Apakah pemikiran saya benar, apakah "tempat untuk memotong hierarki" adalah keputusan rekayasa yang harus saya buat?

Mungkin - saya akan khawatir bahwa Anda akan melakukannya mundur.

Jadi ok, ketika mengembalikan perusahaan, saya jelas tidak mengembalikan seluruh hierarki

Saya tidak berpikir itu jelas sama sekali. Anda harus mengembalikan perwakilan perusahaan yang sesuai untuk kasus penggunaan yang Anda dukung. Kenapa tidak? Apakah benar-benar masuk akal bahwa api tergantung pada komponen kegigihan? Bukankah bagian dari poin bahwa klien tidak perlu terkena pilihan itu dalam implementasi? Apakah Anda akan mempertahankan api yang dikompromikan ketika Anda menukar satu komponen ketekunan dengan yang lain?

Yang mengatakan, jika kasus penggunaan Anda tidak memerlukan seluruh hierarki, tidak perlu mengembalikannya. Dalam dunia yang ideal, api akan menghasilkan representasi perusahaan yang sangat cocok untuk kebutuhan mendesak klien.

Pertanyaan 2: Apakah PUT hanya digunakan untuk sumber daya halus?

Cukup banyak - mengkomunikasikan sifat idempoten perubahan dengan menerapkan sebagai put bagus, tetapi spesifikasi HTTP memungkinkan agen membuat asumsi tentang apa yang sebenarnya terjadi.

Perhatikan komentar ini dari RFC 7231

Permintaan PUT yang diterapkan pada sumber daya target dapat memiliki efek samping pada sumber daya lainnya.

Dengan kata lain, Anda dapat PUT pesan ("sumber daya halus") yang menjelaskan efek samping yang akan dieksekusi pada sumber daya utama Anda (entitas). Anda perlu berhati-hati untuk memastikan implementasi Anda idempoten.

Pertanyaan 3: Apakah ini sekadar keputusan teknis "tahu di mana harus memotong" atau apakah saya kehilangan sesuatu?

Mungkin. Mungkin mencoba untuk memberi tahu Anda bahwa entitas Anda tidak dicakup dengan benar.

Pertanyaan 4: Apakah ini, atau jika tidak, apa "proses berpikir" yang tepat ketika merancang layanan REST dan mencari kompromi antara granularitas sumber daya, kompleksitas kueri, dan kedekatan jaringan?

Ini tidak terasa benar bagi saya, sejauh tampaknya Anda mencoba untuk secara ketat memasangkan skema sumber daya Anda dengan entitas Anda, dan membiarkan pilihan kegigihan Anda mendorong desain Anda, daripada sebaliknya.

HTTP pada dasarnya adalah aplikasi dokumen; jika entitas dalam domain Anda adalah dokumen, maka hebat - tetapi entitas tersebut bukan dokumen, maka Anda harus berpikir. Lihat ceramah Jim Webber : REST in Practice, khususnya mulai dari 36m40-an.

Itu pendekatan sumber daya Anda "berbutir halus".


Dalam jawaban Anda untuk pertanyaan 1, mengapa Anda mengatakan saya mungkin akan mundur?
user3748908

Karena kedengarannya seperti Anda mencoba menyesuaikan persyaratan dengan batasan lapisan kegigihan, alih-alih sebaliknya.
VoiceOfUnreason

2

Secara umum, Anda tidak ingin detail implementasi terbuka di API. msw dan jawaban VoiceofUnason sama-sama mengomunikasikannya, jadi penting untuk meneruskannya.

Ingatlah prinsip ketakjuban , terutama karena Anda khawatir tentang idempoten. Lihatlah beberapa komentar dalam artikel yang Anda posting ( https://stormpath.com/blog/put-or-post/ ); ada banyak ketidaksepakatan di sana tentang bagaimana artikel tersebut menyajikan idempotensi. Gagasan besar yang akan saya ambil dari artikel ini adalah "Permintaan put identik harus menghasilkan hasil yang identik". Yaitu Jika Anda PUT pembaruan untuk nama perusahaan, nama perusahaan berubah dan tidak ada perubahan lain untuk perusahaan itu sebagai hasil dari PUT itu. Permintaan yang sama 5 menit kemudian seharusnya memiliki efek yang sama.

Sebuah pertanyaan menarik untuk dipikirkan (lihat komentar gtrevg di artikel): setiap permintaan PUT, termasuk pembaruan penuh, akan mengubah dateUpdated bahkan jika klien tidak menentukannya. Bukankah itu membuat permintaan PUT melanggar idempotensi?

Jadi kembalilah ke API. Hal-hal umum untuk dipikirkan:

  • Rincian implementasi yang diekspos dalam API harus dihindari
  • Jika implementasinya berubah, API Anda harus tetap intuitif dan mudah digunakan
  • Dokumentasi itu penting
  • Cobalah untuk tidak membengkokkan API untuk mendapatkan peningkatan kinerja

1
samping kecil : idempoten terikat secara kontekstual. Sebagai contoh, proses Logging dan Audit dapat dipicu di dalam PUT dan tindakan-tindakan ini non-idempoten. Tetapi ini adalah detail implementasi internal dan tidak memengaruhi representasi yang diekspos melalui abstraksi layanan; Oleh karena itu sejauh API yang bersangkutan, PUT adalah idempoten.
K. Alan Bates

0

Untuk Q1 Anda tentang di mana harus memotong keputusan teknik, bagaimana kalau mengambil ID unik suatu entitas yang dengan cara lain akan memberi Anda rincian yang diperlukan di backend? Misalnya, "perusahaan / 1 / departemen / 1" akan memiliki Pengidentifikasi unik sendiri (atau kita dapat memiliki satu untuk mewakili yang sama) untuk memberi Anda hierarki, Anda dapat menggunakannya.

Untuk Q3 Anda di PUT dengan informasi kembung penuh, Anda dapat menandai bidang yang diperbarui dan mengirim informasi metadata tambahan ke server untuk Anda introspeksi dan memperbarui bidang itu sendiri.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.