Ich habe einige Tutorials zum Entwerfen von REST-APIs befolgt, aber ich habe immer noch einige große Fragezeichen. Alle diese Tutorials zeigen Ressourcen mit relativ einfachen Hierarchien, und ich würde gerne wissen, wie die darin verwendeten Prinzipien auf komplexere angewendet werden. Darüber hinaus bleiben sie auf einem sehr hohen / architektonischen Niveau. Sie zeigen kaum relevanten Code, geschweige denn die Persistenzschicht. Ich bin besonders besorgt über das Laden / die Leistung der Datenbank, wie Gavin King sagte :
Sie sparen sich Mühe, wenn Sie in allen Entwicklungsphasen auf die Datenbank achten
Angenommen, meine Bewerbung bietet Schulungen für Companies
. Companies
haben Departments
und Offices
. Departments
haben Employees
. Employees
haben Skills
und Courses
, und bestimmte Level
Fähigkeiten sind erforderlich, um für einige Kurse unterschreiben zu können. Die Hierarchie ist wie folgt, aber mit:
-Companies
-Departments
-Employees
-PersonalInformation
-Address
-Skills (quasi-static data)
-Levels (quasi-static data)
-Courses
-Address
-Offices
-Address
Pfade wären etwas wie:
companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1
Eine Ressource abrufen
Also ok, wenn ein Unternehmen zurückkehrt, kehre ich natürlich nicht die ganze Hierarchie companies/1/departments/1/employees/1/courses/1
+ companies/1/offices/../
. Ich kann eine Liste mit Links zu den Abteilungen oder den erweiterten Abteilungen zurückgeben und muss auf dieser Ebene dieselbe Entscheidung treffen: Gebe ich eine Liste mit Links zu den Mitarbeitern der Abteilung oder den erweiterten Mitarbeitern zurück? Das hängt von der Anzahl der Abteilungen, Mitarbeiter usw. ab.
Frage 1 : Ist mein Denken richtig? Ist "Wo soll ich die Hierarchie abschneiden?" Eine typische technische Entscheidung, die ich treffen muss?
Nehmen wir nun an GET companies/id
, ich entscheide mich auf Anfrage , eine Liste mit Links zur Abteilungssammlung und den erweiterten Büroinformationen zurückzugeben. Meine Unternehmen haben nicht viele Büros, also sollten Sie sich an die Tische setzen Offices
und Addresses
sollten keine große Sache sein. Beispiel für eine Antwort:
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
}
}
}
Auf Codeebene bedeutet dies, dass ich (mit Hibernate bin ich mir nicht sicher, wie es mit anderen Anbietern ist, aber ich denke, das ist ziemlich dasselbe) ich eine Sammlung von nicht Department
als Feld in meine Company
Klasse aufnehmen werde, weil:
- Wie gesagt, ich
Company
lade es nicht mit , also möchte ich es nicht eifrig laden - Und wenn ich es nicht eifrig lade, kann ich es auch entfernen, da der Persistenzkontext nach dem Laden einer Firma geschlossen wird und es keinen Sinn macht, danach zu versuchen, es zu laden (
LazyInitializationException
).
Dann werde ich eine Integer companyId
in die Department
Klasse aufnehmen, damit ich einer Firma eine Abteilung hinzufügen kann.
Außerdem muss ich die IDs aller Abteilungen erhalten. Ein weiterer Treffer für die DB, aber kein schwerer, sollte also in Ordnung sein. Der Code könnte folgendermaßen aussehen:
@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
}
Aktualisieren einer Ressource
Für den Aktualisierungsvorgang kann ich einen Endpunkt mit PUT
oder verfügbar machen POST
. Da ich möchte PUT
, dass ich idempotent bin, kann ich keine Teilaktualisierungen zulassen . Wenn ich jedoch das Beschreibungsfeld des Unternehmens ändern möchte, muss ich die gesamte Ressourcendarstellung senden. Das scheint zu aufgebläht. Das gleiche gilt für die Aktualisierung eines Mitarbeiters PersonalInformation
. Ich denke nicht, dass es Sinn macht, alle Skills
+ Courses
zusammen damit zu senden .
Frage 2 : Wird PUT nur für feinkörnige Ressourcen verwendet?
Ich habe in den Protokollen gesehen, dass Hibernate beim Zusammenführen einer Entität eine Reihe von SELECT
Abfragen ausführt . Ich denke, das ist nur, um zu überprüfen, ob sich etwas geändert hat, und um alle benötigten Informationen zu aktualisieren. Je höher die Entität in der Hierarchie ist, desto schwerer und komplexer sind die Abfragen. Einige Quellen empfehlen jedoch, grobkörnige Ressourcen zu verwenden. Daher muss ich erneut überprüfen, wie viele Tabellen zu viel sind, und einen Kompromiss zwischen Ressourcengranularität und Komplexität der DB-Abfragen finden.
Frage 3 : Ist dies nur eine weitere technische Entscheidung, bei der Sie wissen, wo Sie schneiden müssen, oder fehlt mir etwas?
Frage 4 : Ist dies oder wenn nicht, was ist der richtige "Denkprozess", wenn Sie einen REST-Service entwerfen und nach einem Kompromiss zwischen Ressourcengranularität, Abfragekomplexität und Netzwerk-Chat suchen?
Antworten:
Ich denke, Sie haben Komplexität, weil Sie mit Überkomplikation beginnen:
Stattdessen würde ich ein einfacheres URL-Schema wie das folgende einführen:
Auf diese Weise werden die meisten Ihrer Fragen beantwortet - Sie "schneiden" die Hierarchie sofort und binden Ihr URL-Schema nicht an die interne Datenstruktur. Wenn wir beispielsweise die Mitarbeiter-ID kennen, würden Sie erwarten, dass Sie sie wie
employees/:ID
oder wie abfragencompanies/:X/departments/:Y/employees/:ID
?In Bezug auf
PUT
vs-POST
Anfragen geht aus Ihrer Frage hervor, dass Sie der Meinung sind, dass die teilweisen Aktualisierungen für Ihre Daten effizienter sind. Also würde ich einfachPOST
s verwenden.In der Praxis möchten Sie tatsächlich Datenlesevorgänge (
GET
Anforderungen) zwischenspeichern, und dies ist für Datenaktualisierungen weniger wichtig. Und Updates können oft nicht zwischengespeichert werden, unabhängig davon, welche Art von Anforderung Sie ausführen (z. B. wenn der Server die Aktualisierungszeit automatisch festlegt - sie ist für jede Anforderung unterschiedlich).Update: In Bezug auf den richtigen "Denkprozess" - da er auf HTTP basiert, können wir beim Entwerfen der Website-Struktur die normale Denkweise anwenden. In diesem Fall können wir oben eine Liste von Unternehmen haben und jeweils eine kurze Beschreibung mit einem Link zur Seite "Unternehmen anzeigen" anzeigen, auf der Unternehmensdetails und Links zu Büros / Abteilungen usw. angezeigt werden.
quelle
IMHO, ich denke, Sie verpassen den Punkt.
Erstens hängen die REST-API und die DB-Leistung nicht zusammen .
Die REST-API ist nur eine Schnittstelle , sie definiert überhaupt nicht, wie Sie Dinge unter der Haube tun. Sie können es jeder DB-Struktur zuordnen, die Sie dahinter mögen. Deshalb:
Das ist es.
... und schließlich riecht es nach vorzeitiger Optimierung. Halten Sie es einfach, probieren Sie es aus und passen Sie es gegebenenfalls an.
quelle
Vielleicht - ich würde mir Sorgen machen, dass Sie es rückwärts machen.
Ich denke nicht, dass das überhaupt offensichtlich ist. Sie sollten Unternehmensrepräsentationen zurücksenden, die für die von Ihnen unterstützten Anwendungsfälle geeignet sind. Warum würdest du nicht? Ist es wirklich sinnvoll, dass die API von der Persistenzkomponente abhängt? Ist es nicht Teil des Punktes, dass die Clients bei der Implementierung nicht dieser Auswahl ausgesetzt sein müssen? Werden Sie eine kompromittierte API beibehalten, wenn Sie eine Persistenzkomponente gegen eine andere austauschen?
Wenn Ihre Anwendungsfälle jedoch nicht die gesamte Hierarchie benötigen, müssen Sie sie nicht zurückgeben. In einer idealen Welt würde die API Darstellungen von Unternehmen erstellen, die perfekt auf die unmittelbaren Bedürfnisse des Kunden zugeschnitten sind.
Ziemlich genau - die Kommunikation der idempotenten Natur einer Änderung durch Implementierung als Put ist nett, aber die HTTP-Spezifikation ermöglicht es Agenten, Annahmen darüber zu treffen, was wirklich passiert.
Beachten Sie diesen Kommentar von RFC 7231
Mit anderen Worten, Sie können eine Nachricht (eine "feinkörnige Ressource") einfügen, die einen Nebeneffekt beschreibt, der auf Ihrer primären Ressource (Entität) ausgeführt werden soll. Sie müssen einige Sorgfalt walten lassen, um sicherzustellen, dass Ihre Implementierung idempotent ist.
Könnte sein. Möglicherweise wird versucht, Ihnen mitzuteilen, dass Ihre Entitäten nicht den richtigen Umfang haben.
Dies fühlt sich für mich nicht richtig an, da Sie anscheinend versuchen, Ihr Ressourcenschema eng mit Ihren Entitäten zu koppeln, und Ihre Wahl der Persistenz Ihr Design bestimmen lässt und nicht umgekehrt.
HTTP ist im Grunde eine Dokumentanwendung. Wenn die Entitäten in Ihrer Domain Dokumente sind, dann großartig - aber die Entitäten sind keine Dokumente, dann müssen Sie nachdenken. Siehe Jim Webbers Vortrag: REST in der Praxis, insbesondere ab 36:40 Uhr.
Das ist Ihr "feinkörniger" Ressourcenansatz.
quelle
Im Allgemeinen möchten Sie keine Implementierungsdetails in der API verfügbar machen. Die Antworten von msw und VoiceofUnreason kommunizieren dies beide, daher ist es wichtig, darauf einzugehen.
Denken Sie an das Prinzip des geringsten Erstaunens , zumal Sie sich Sorgen um Idempotenz machen. Schauen Sie sich einige der Kommentare in dem Artikel an, den Sie veröffentlicht haben ( https://stormpath.com/blog/put-or-post/ ). Es gibt dort viele Meinungsverschiedenheiten darüber, wie der Artikel Idempotenz darstellt. Die große Idee, die ich aus dem Artikel herausnehmen würde, ist, dass "identische Put-Anfragen identische Ergebnisse verursachen sollten". Dh wenn Sie eine Aktualisierung des Firmennamens PUTEN, ändert sich der Firmenname und nichts anderes ändert sich für dieses Unternehmen infolge dieses PUT. Dieselbe Anfrage 5 Minuten später sollte den gleichen Effekt haben.
Eine interessante Frage, über die Sie nachdenken sollten (lesen Sie den Kommentar von gtrevg im Artikel): Jede PUT-Anforderung, einschließlich einer vollständigen Aktualisierung, ändert dateUpdated, auch wenn ein Client dies nicht angibt. Würde das nicht dazu führen, dass eine PUT-Anfrage die Idempotenz verletzt?
Also zurück zur API. Allgemeine Dinge zum Nachdenken:
quelle
Wie wäre es, wenn Sie in Ihrem ersten Quartal festlegen, wo die technischen Entscheidungen getroffen werden sollen, die eindeutige ID einer Entität ermitteln, die Ihnen auf andere Weise die erforderlichen Details zum Backend liefert? Zum Beispiel hat "Unternehmen / 1 / Abteilung / 1" eine eigene eindeutige Kennung (oder wir können eine haben, die dieselbe darstellt), um Ihnen die Hierarchie zu geben. Sie können diese verwenden.
Für Ihr Q3 auf PUT mit vollständig aufgeblähten Informationen können Sie die aktualisierten Felder markieren und diese zusätzlichen Metadateninformationen an den Server senden, damit Sie diese Felder allein überprüfen und aktualisieren können.
quelle