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?
Respuestas:
Creo que tienes complejidad porque estás comenzando con una complicación excesiva:
En su lugar, introduciría un esquema de URL más simple como este:
De esta forma, responde a la mayoría de sus preguntas: "corta" la jerarquía de inmediato y no vincula su esquema de URL a la estructura de datos interna. Por ejemplo, si conocemos la identificación del empleado, ¿esperaría consultarlo como
employees/:ID
o comocompanies/:X/departments/:Y/employees/:ID
?Con respecto a las solicitudes
PUT
vsPOST
, a partir de su pregunta, está claro que cree que las actualizaciones parciales serán más eficientes para sus datos. Así que solo usaríaPOST
s.En la práctica, realmente desea almacenar en caché las lecturas de datos (
GET
solicitudes) y es menos importante para las actualizaciones de datos. Y las actualizaciones a menudo no se pueden almacenar en caché, independientemente del tipo de solicitud que realice (como si el servidor establece automáticamente el tiempo de actualización, será diferente para cada solicitud).Actualización: con respecto al "proceso de pensamiento" correcto, ya que se basa en HTTP, podemos aplicar la forma habitual de pensar al diseñar la estructura del sitio web. En este caso, en la parte superior, podemos tener una lista de compañías y mostrar una breve descripción de cada una con un enlace a la página "ver compañía", donde mostramos detalles de la compañía y enlaces a oficinas / departamentos, etc.
fuente
En mi humilde opinión, creo que te estás perdiendo el punto.
Primero, la API REST y el rendimiento de la base de datos no están relacionados .
La API REST es solo una interfaz , no define en absoluto cómo se hacen las cosas bajo el capó. Puede asignarlo a cualquier estructura de base de datos que desee detrás de él. Por lo tanto:
Eso es.
... y, por último, esto huele a optimización prematura. Hazlo simple, pruébalo y adáptalo si es necesario.
fuente
Tal vez, sin embargo, me preocuparía que lo estés haciendo al revés.
No creo que sea obvio en absoluto. Debería devolver las representaciones de la compañía adecuadas para los casos de uso que está respaldando. ¿Por qué no lo harías? ¿Realmente tiene sentido que la API dependa del componente de persistencia? ¿No es parte del punto que los clientes no necesitan estar expuestos a esa elección en la implementación? ¿Va a preservar una API comprometida cuando cambie un componente de persistencia por otro?
Dicho esto, si sus casos de uso no necesitan toda la jerarquía, no hay necesidad de devolverla. En un mundo ideal, la API produciría representaciones de la compañía que se adapten perfectamente a las necesidades inmediatas del cliente.
Prácticamente, comunicar la naturaleza idempotente de un cambio mediante la implementación como un put es bueno, pero la especificación HTTP permite a los agentes hacer suposiciones sobre lo que realmente está sucediendo.
Tenga en cuenta este comentario de RFC 7231
En otras palabras, puede PONER un mensaje (un "recurso de grano fino") que describe un efecto secundario que se ejecutará en su recurso (entidad) primario. Debe tener cuidado para asegurarse de que su implementación sea idempotente.
Tal vez. Podría estar tratando de decirle que sus entidades no tienen un alcance correcto.
Esto no me parece correcto, en la medida en que parece que está tratando de acoplar estrechamente su esquema de recursos a sus entidades, y está permitiendo que su elección de persistencia impulse su diseño, en lugar de al revés.
HTTP es fundamentalmente una aplicación de documentos; si las entidades en tu dominio son documentos, entonces genial, pero las entidades no son documentos, entonces debes pensar. Vea la charla de Jim Webber : REST en la práctica, particularmente a partir de 36m40s.
Ese es su enfoque de recursos "de grano fino".
fuente
En general, no desea que se expongan detalles de implementación en la API. Las respuestas de msw y VoiceofUnreason están comunicando eso, por lo que es importante entender.
Tenga en cuenta el principio del mínimo asombro , especialmente porque le preocupa la idempotencia. Eche un vistazo a algunos de los comentarios en el artículo que publicó ( https://stormpath.com/blog/put-or-post/ ); Hay un gran desacuerdo sobre cómo el artículo presenta idempotencia. La gran idea que sacaría del artículo es que "las solicitudes de colocación idénticas deberían causar resultados idénticos". Es decir, si PONE una actualización al nombre de una compañía, el nombre de la compañía cambia y nada más cambia para esa compañía como resultado de esa PUT. La misma solicitud 5 minutos después debería tener el mismo efecto.
Una pregunta interesante para pensar (consulte el comentario de gtrevg en el artículo): cualquier solicitud PUT, incluida una actualización completa, modificará dateUpdated incluso si un cliente no lo especifica. ¿No haría eso que una solicitud PUT violara la idempotencia?
Así que de vuelta a la API. Cosas generales en las que pensar:
fuente
Para su Q1 sobre dónde cortar las decisiones de ingeniería, ¿qué tal si elige el ID único de una entidad que de otra manera le daría los detalles requeridos en el back-end? Por ejemplo, "compañías / 1 / departamento / 1" tendrá un Identificador único (o podemos tener uno para representar lo mismo) para darle la jerarquía, puede usar eso.
Para su Q3 en PUT con información completa, puede marcar los campos que se actualizaron y enviar esa información de metadatos adicional al servidor para que pueda introspectar y actualizar esos campos solo.
fuente