Ho seguito alcuni tutorial su come progettare le API REST, ma ho ancora alcuni grandi interrogativi. Tutti questi tutorial mostrano risorse con gerarchie relativamente semplici e vorrei sapere come i principi usati in quelli si applicano a quelli più complessi. Inoltre, rimangono ad un livello molto alto / architettonico. Mostrano a malapena qualsiasi codice rilevante, per non parlare dello strato di persistenza. Sono particolarmente preoccupato per il carico / le prestazioni del database, come ha affermato Gavin King :
you will save yourself effort if you pay attention to the database at all stages of development
Supponiamo che la mia applicazione fornisca corsi di formazione per Companies
. Companies
ha Departments
e Offices
. Departments
ha Employees
. Employees
ha Skills
e Courses
, e alcuni Level
di determinate abilità sono richiesti per poter firmare per alcuni corsi. La gerarchia è come segue, ma con:
-Companies
-Departments
-Employees
-PersonalInformation
-Address
-Skills (quasi-static data)
-Levels (quasi-static data)
-Courses
-Address
-Offices
-Address
I percorsi sarebbero qualcosa come:
companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1
Recupero di una risorsa
Quindi ok, quando restituisco un'azienda, ovviamente non restituisco l'intera gerarchia companies/1/departments/1/employees/1/courses/1
+ companies/1/offices/../
. Potrei restituire un elenco di collegamenti ai reparti o ai reparti ampliati e dover prendere la stessa decisione a questo livello: posso restituire un elenco di collegamenti ai dipendenti del dipartimento o ai dipendenti espansi? Ciò dipenderà dal numero di dipartimenti, impiegati, ecc.
Domanda 1 : il mio modo di pensare è corretto, è "dove tagliare la gerarchia" una tipica decifrazione ingegneristica che devo fare?
Ora diciamo che quando viene richiesto GET companies/id
, decido di restituire un elenco di collegamenti alla raccolta del dipartimento e le informazioni sull'ufficio espanso. Le mie aziende non hanno molti uffici, quindi entrare a far parte delle tabelle Offices
e Addresses
non dovrebbe essere un grosso problema. Esempio di risposta:
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
}
}
}
A livello di codice, questo implica che (usando Hibernate, non sono sicuro di come sia con altri provider, ma suppongo che sia praticamente lo stesso) Non inserirò una raccolta di Department
come campo nella mia classe Company
, perché:
- Come detto, non lo sto caricando con
Company
, quindi non voglio caricarlo con entusiasmo - E se non lo caricherò avidamente, potrei anche rimuoverlo, perché il contesto di persistenza si chiuderà dopo che avrò caricato una società e non c'è motivo di provare a caricarlo in seguito (
LazyInitializationException
).
Quindi inserirò un Integer companyId
nella classe Department
, in modo che possa aggiungere un reparto a una società.
Inoltre, ho bisogno di ottenere gli ID di tutti i reparti. Un altro colpo al DB ma non pesante, quindi dovrebbe essere ok. Il codice potrebbe essere simile a:
@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
}
Aggiornamento di una risorsa
Per l'operazione di aggiornamento, posso esporre un endpoint con PUT
o POST
. Poiché desidero che il mio PUT
sia idempotente, I non può consentire aggiornamenti parziali . Ma poi, se voglio modificare il campo della descrizione dell'azienda, devo inviare l'intera rappresentazione della risorsa. Sembra troppo gonfio. Lo stesso quando si aggiorna PersonalInformation
di un dipendente. Non penso che abbia senso dover inviare tutti Skills
+ Courses
insieme a quello.
Domanda 2 : PUT è appena usato per risorse a grana fine?
Ho visto nei registri che, durante la fusione di un'entità, Hibernate esegue una serie di query SELECT
. Immagino che sia solo per controllare se qualcosa è cambiato e aggiornare qualsiasi informazione necessaria. Più in alto è l'entità nella gerarchia, più pesanti e complesse le query. Ma alcune fonti consigliano di utilizzare risorse a grana grossa . Quindi, ancora una volta, dovrò controllare quante tabelle sono troppe e trovare un compromesso tra granularità delle risorse e complessità delle query del DB.
Domanda 3 : si tratta solo di un'altra decisione di "sapere dove tagliare" o mi manca qualcosa?
Domanda 4 : è questa o no, qual è il giusto "processo di pensiero" quando si progetta un servizio REST e si cerca un compromesso tra granularità delle risorse, complessità delle query e chattiness di rete?