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?