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
. Companies
miliki Departments
dan Offices
. Departments
miliki Employees
. Employees
miliki Skills
dan Courses
, dan Level
keterampilan 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 Offices
dan Addresses
seharusnya 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 Department
sebagai bidang di Company
kelas 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 companyId
di Department
kelas, 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 PUT
atau POST
. Karena saya ingin saya PUT
menjadi 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
+ Courses
bersama-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 SELECT
query. 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?
Jawaban:
Saya pikir Anda memiliki kompleksitas karena Anda mulai dengan komplikasi berlebihan:
Alih-alih, saya akan memperkenalkan skema URL yang lebih sederhana seperti ini:
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/:ID
atau sukacompanies/:X/departments/:Y/employees/:ID
?Mengenai
PUT
vsPOST
permintaan, dari pertanyaan Anda jelas bahwa Anda merasa pembaruan parsial akan lebih efisien untuk data Anda. Jadi saya hanya akan menggunakanPOST
s.Dalam praktiknya, Anda sebenarnya ingin menyimpan data (
GET
permintaan) 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.
sumber
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:
Itu dia.
... dan terakhir, baunya seperti optimasi prematur. Tetap sederhana, cobalah, dan sesuaikan jika perlu.
sumber
Mungkin - saya akan khawatir bahwa Anda akan melakukannya mundur.
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.
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
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.
Mungkin. Mungkin mencoba untuk memberi tahu Anda bahwa entitas Anda tidak dicakup dengan benar.
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".
sumber
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:
sumber
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.
sumber