He estado siguiendo algunos tutoriales sobre cómo diseñar API REST, pero todavía tengo algunos interrogantes importantes. Todos estos tutoriales muestran recursos con jerarquías relativamente simples, y me gustaría saber cómo se aplican los principios utilizados en ellos a uno más complejo. Además, se mantienen en un nivel muy alto / arquitectónico. Apenas muestran código relevante, y mucho menos la capa de persistencia. Me preocupa especialmente la carga / rendimiento de la base de datos, como dijo Gavin King :
ahorrará esfuerzo si presta atención a la base de datos en todas las etapas de desarrollo
Digamos que mi aplicación proporcionará capacitación para Companies
. Companies
tener Departments
y Offices
. Departments
tener Employees
. Employees
tener Skills
y Courses
, y algunas Level
de ciertas habilidades son necesarias para poder inscribirse en algunos cursos. La jerarquía es la siguiente, pero con:
-Companies
-Departments
-Employees
-PersonalInformation
-Address
-Skills (quasi-static data)
-Levels (quasi-static data)
-Courses
-Address
-Offices
-Address
Los caminos serían algo así como:
companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1
Obteniendo un recurso
Entonces, cuando devuelvo una empresa, obviamente no devuelvo toda la jerarquía companies/1/departments/1/employees/1/courses/1
+ companies/1/offices/../
. Podría devolver una lista de enlaces a los departamentos o departamentos ampliados, y tengo que tomar la misma decisión en este nivel: ¿devuelvo una lista de enlaces a los empleados del departamento o los empleados ampliados? Eso dependerá de la cantidad de departamentos, empleados, etc.
Pregunta 1 : ¿Es correcto mi pensamiento? ¿Es "dónde cortar la jerarquía" una decisión de ingeniería típica que debo tomar?
Ahora, digamos que cuando me preguntan GET companies/id
, decido devolver una lista de enlaces a la colección del departamento y la información ampliada de la oficina. Mis empresas no tienen muchas oficinas, por lo que unirse a las mesas Offices
y Addresses
no debería ser un gran problema. Ejemplo de respuesta:
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 nivel de código, esto implica que (usando Hibernate, no estoy seguro de cómo es con otros proveedores, pero supongo que es más o menos lo mismo) no pondré una colección Department
como un campo en mi Company
clase, porque:
- Como dije, no lo estoy cargando
Company
, así que no quiero cargarlo ansiosamente - Y si no lo cargo con entusiasmo, también podría eliminarlo, porque el contexto de persistencia se cerrará después de cargar una empresa y no tiene sentido intentar cargarlo después (
LazyInitializationException
).
Luego, pondré un Integer companyId
en la Department
clase, para que pueda agregar un departamento a una empresa.
Además, necesito obtener los identificadores de todos los departamentos. Otro golpe al DB pero no uno pesado, por lo que debería estar bien. El código podría verse así:
@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
}
Actualizando un recurso
Para la operación de actualización, puedo exponer un punto final con PUT
o POST
. Como quiero PUT
que sea idempotente, no puedo permitir actualizaciones parciales . Pero luego, si quiero modificar el campo de descripción de la compañía, necesito enviar la representación completa del recurso. Eso parece demasiado hinchado. Lo mismo cuando se actualiza el de un empleado PersonalInformation
. No creo que tenga sentido enviar todos los Skills
+ Courses
junto con eso.
Pregunta 2 : ¿Se utiliza PUT solo para recursos de grano fino?
He visto en los registros que, al fusionar una entidad, Hibernate ejecuta un montón de SELECT
consultas. Supongo que eso es solo para verificar si algo ha cambiado y actualizar la información necesaria. Cuanto más alta es la entidad en la jerarquía, más pesadas y complejas son las consultas. Pero algunas fuentes aconsejan utilizar recursos de grano grueso . Entonces, nuevamente, tendré que verificar cuántas tablas son demasiadas y encontrar un compromiso entre la granularidad de recursos y la complejidad de la consulta de base de datos.
Pregunta 3 : ¿Es esta otra decisión de ingeniería de "saber dónde cortar" o me estoy perdiendo algo?
Pregunta 4 : ¿Es este, o si no, cuál es el "proceso de pensamiento" correcto al diseñar un servicio REST y buscar un compromiso entre la granularidad de los recursos, la complejidad de las consultas y el chat de la red?