¿Cómo diseñar una API REST compleja considerando el rendimiento de la base de datos?


8

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. Companiestener Departmentsy Offices. Departmentstener Employees. Employeestener Skillsy Courses, y algunas Levelde 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 Officesy Addressesno 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 Departmentcomo un campo en mi Companyclase, 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 companyIden la Departmentclase, 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 PUTo POST. Como quiero PUTque 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+ Coursesjunto 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 SELECTconsultas. 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?


1
1. Sí Debido a que las llamadas REST son caras, es importante tratar de obtener la granularidad correcta.
Robert Harvey

1
2. No. El verbo PUT no tiene nada que ver con la granularidad, per se.
Robert Harvey

1
3. Sí No, no te estás perdiendo nada.
Robert Harvey

1
4. La idea correcta es "hacer lo que mejor satisfaga sus requisitos de escalabilidad, rendimiento, mantenibilidad y otros problemas". Esto podría requerir algo de experimentación para encontrar el punto óptimo.
Robert Harvey

44
Demasiado largo. No leí. ¿Se puede dividir esto en 4 preguntas reales?
MetaFight

Respuestas:


7

Creo que tienes complejidad porque estás comenzando con una complicación excesiva:

Los caminos serían algo así como:

companies/1/departments/1/employees/1/courses/1
companies/1/offices/1/employees/1/courses/1

En su lugar, introduciría un esquema de URL más simple como este:

GET companies/
    Returns a list of companies, for each company 
    return short essential info (ID, name, maybe industry)
GET companies/1
    Returns single company info like this:

    {
        "name":"Acme",
        "description":"Some text here"
        "industry":"Manufacturing"
        departments: {
            "href":"/companies/1/departments"
            "count": 5
        }
        offices: {
            "href":"/companies/1/offices"
            "count": 3
        }
    }

    We don't expand the data for internal sub-resources, 
    just return the count, so client knows that some data is present.
    In some cases count may be not needed too.
GET companies/1/departments
    Returns company departments, again short info for each department
GET departments/
    Here you need to decide if it makes sense to expose 
    a list of departments or not. 
    If not - leave only companies/X/departments method.

    Note, that you can also use query string to make this 
    method "searchable", like:
        /departments?company=1 - list of all departments for company 1
        /departments?type=support - all 'support' departments for all companies
GET departments/1
    Returns department 1 data

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/:IDo como companies/:X/departments/:Y/employees/:ID?

Con respecto a las solicitudes PUTvs POST, 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ía POSTs.

En la práctica, realmente desea almacenar en caché las lecturas de datos ( GETsolicitudes) 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.


5

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:

  1. diseñar su API para que sea fácil para el usuario
  2. diseñe su base de datos para que pueda escalar razonablemente:
    • asegúrese de tener los índices correctos
    • si almacena objetos, solo asegúrese de que no sean demasiado grandes.

Eso es.

... y, por último, esto huele a optimización prematura. Hazlo simple, pruébalo y adáptalo si es necesario.


2

Pregunta 1: ¿Es correcto mi pensamiento? ¿Es "dónde cortar la jerarquía" una decisión de ingeniería típica que debo tomar?

Tal vez, sin embargo, me preocuparía que lo estés haciendo al revés.

Entonces, cuando devuelvo una empresa, obviamente no devuelvo toda la jerarquía

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.

Pregunta 2: ¿Se utiliza PUT solo para recursos de grano fino?

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

Una solicitud PUT aplicada al recurso de destino puede tener efectos secundarios en otros recursos.

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.

Pregunta 3: ¿Es esta otra decisión de ingeniería de "saber dónde cortar" o me estoy perdiendo algo?

Tal vez. Podría estar tratando de decirle que sus entidades no tienen un alcance correcto.

Pregunta 4: ¿Es este, o si no, cuál es el "proceso de pensamiento" correcto cuando se diseña un servicio REST y se busca un compromiso entre la granularidad de los recursos, la complejidad de las consultas y el chat de la red?

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".


En su respuesta a la pregunta 1, ¿por qué dice que podría estar retrocediendo?
user3748908

Porque sonaba como si intentara ajustar los requisitos a la restricción de la capa de persistencia, en lugar de al revés.
VoiceOfUnreason

2

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:

  • Se deben evitar los detalles de implementación expuestos en la API
  • Si la implementación cambia, su API debería ser intuitiva y fácil de usar.
  • La documentación es importante
  • Intenta no deformar la API para obtener mejoras de rendimiento

1
menor aparte : la idempotencia está ligada contextualmente. Como ejemplo, los procesos de registro y auditoría se pueden activar dentro de un PUT y estas acciones no son idempotentes. Pero estos son detalles de implementación internos y no afectan las representaciones que se exponen a través de la abstracción del servicio; por lo tanto, en lo que respecta a la API , el PUT es idempotente.
K. Alan Bates

0

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.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.