Come progettare un'API REST complessa considerando le prestazioni del DB?

8

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?

    
posta user3748908 19.01.2016 - 17:15
fonte

5 risposte

7

Penso che tu abbia complessità perché stai iniziando con una complicazione eccessiva:

Paths would be something as:

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

Invece vorrei introdurre uno schema URL più semplice come questo:

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

In questo modo risponde alla maggior parte delle tue domande: tu "tagli" la gerarchia immediatamente e non leghi il tuo schema URL alla struttura interna dei dati. Ad esempio, se conosciamo l'ID dipendente, ti aspetti di interrogarlo come employees/:ID o come companies/:X/departments/:Y/employees/:ID ?

Per quanto riguarda le richieste di PUT rispetto a POST , dalla tua domanda è chiaro che ritieni che gli aggiornamenti parziali saranno più efficienti per i tuoi dati. Quindi vorrei usare POST s.

In pratica, in realtà si desidera memorizzare nella cache le letture dei dati (% richieste diGET) ed è meno importante per gli aggiornamenti dei dati. E spesso gli aggiornamenti non possono essere memorizzati nella cache indipendentemente dal tipo di richiesta (come se il server impostasse automaticamente il tempo di aggiornamento - sarà diverso per ogni richiesta).

Aggiornamento: per quanto riguarda il giusto "processo di pensiero" - dal momento che si basa su HTTP, possiamo applicare il modo normale di pensare quando si progetta la struttura del sito web. In questo caso, in cima, possiamo avere un elenco di aziende e mostrare una breve descrizione per ciascuna con un link alla pagina "visualizza società", dove mostriamo i dettagli della società e i link agli uffici / dipartimenti e così via.

    
risposta data 26.01.2016 - 14:47
fonte
4

IMHO, penso che manchi il punto.

Innanzitutto, l'API REST e le prestazioni del DB sono non correlate .

L'API REST è solo un'interfaccia , non definisce affatto come fai roba sotto il cofano. Puoi mapparlo a qualsiasi struttura DB che ti piace dietro. Pertanto:

  1. progetta l'API in modo che sia facile per l'utente
  2. progetta il tuo database in modo che possa scalare ragionevolmente:
    • assicurati di avere gli indici corretti
    • se archivi oggetti, assicurati che non siano troppo grandi.

Questo è tutto.

... e infine, questo odora di ottimizzazione prematura. Resta semplice, provalo e adattalo se necessario.

    
risposta data 22.01.2016 - 21:42
fonte
2

Question 1: Is my thinking correct, is "where to cut the hierarchy" a typical engineering decission I need to make?

Forse - sarei preoccupato che tu stia andando indietro, comunque.

So ok, when returning a company, I obviously don't return the whole hierarchy

Non penso che sia ovvio. Dovresti restituire la / le rappresentanza / i della compagnia appropriata per i casi d'uso che stai sostenendo. Perché non dovresti? Ha davvero senso che l'API dipenda dal componente di persistenza? Non fa parte del fatto che i clienti non debbano essere esposti a tale scelta nell'implementazione? Hai intenzione di preservare un'API compromessa quando si sostituisce un componente di persistenza con un altro?

Detto questo, se i tuoi casi d'uso non hanno bisogno dell'intera gerarchia, non è necessario restituirlo. In un mondo ideale, l'API produrrebbe rappresentazioni di società che si adattano perfettamente alle esigenze immediate del cliente.

Question 2: Is PUT just used for fine-grained resources?

Praticamente - comunicare la natura idempotente di un cambiamento implementandolo come put è bello, ma la specifica HTTP consente agli agenti di formulare ipotesi su ciò che sta realmente accadendo.

Nota questo commento da RFC 7231

A PUT request applied to the target resource can have side effects on other resources.

In altre parole, puoi mettere un messaggio (una "risorsa a grana fine") che descrive un effetto collaterale da eseguire sulla tua risorsa primaria (entità). Devi fare molta attenzione per assicurarti che la tua implementazione sia idempotente.

Question 3: Is this just another "know where to cut" engineering decission or am I missing something?

Forse. Potrebbe provare a dirti che le tue entità non hanno un ambito corretto.

Question 4: Is this, or if not, what is the right "thinking process" when designing a REST service and searching for a compromise between resource granularity, query complexity and network chattiness?

Questo non mi sembra giusto, in quanto sembra che tu stia cercando di accoppiare strettamente il tuo schema di risorse alle tue entità, e stai lasciando che la tua scelta di persistenza guidi il tuo design, piuttosto che il contrario.

HTTP è fondamentalmente un'applicazione per documenti; se le entità nel tuo dominio sono documenti, allora è grandioso, ma le entità non sono documenti, quindi devi pensare. Vedi Talk di Jim Webber: REST in pratica, in particolare a partire da 36m40s.

Questo è il tuo approccio alle risorse "a grana fine".

    
risposta data 19.01.2016 - 22:03
fonte
2

In generale, non vuoi che i dettagli di implementazione vengano esposti nell'API. Le risposte di msw e VoiceofUnreason stanno entrambi comunicando questo, quindi è importante riprenderlo.

Tieni presente il principio di minimo stupore , soprattutto perché sei preoccupato dell'idempotenza. Dai un'occhiata ad alcuni dei commenti nell'articolo che hai postato ( link ); c'è molto disaccordo sul modo in cui l'articolo presenta l'idempotenza. La grande idea che vorrei togliere dall'articolo è che "le richieste di inserimento identico dovrebbero causare risultati identici". Cioè Se si aggiunge un aggiornamento al nome di una società, il nome dell'azienda cambia e nient'altro cambia per quella società come risultato di tale PUT. La stessa richiesta 5 minuti dopo dovrebbe avere lo stesso effetto.

Una domanda interessante su cui riflettere (controlla il commento di gtrevg nell'articolo): qualsiasi richiesta PUT, incluso un aggiornamento completo, modificherà dateUpdated anche se un client non lo specifica. Non farebbe in modo che nessuna richiesta PUT violi l'idempotenza?

Quindi torna all'API. Cose generali a cui pensare:

  • I dettagli sull'implementazione esposti nell'API dovrebbero essere evitati
  • Se l'implementazione cambia, la tua API dovrebbe essere ancora intuitiva e facile da usare
  • La documentazione è importante
  • Cerca di non alterare l'API per ottenere miglioramenti delle prestazioni
risposta data 23.01.2016 - 01:24
fonte
0

Per il tuo Q1 su dove tagliare le decisioni di ingegneria, che ne dici di raccogliere l'ID univoco di un'entità che in altro modo ti darà i dettagli richiesti sul back-end? Ad esempio, "aziende / 1 / dipartimento / 1" avrà un identificatore univoco (o possiamo averne uno per rappresentare lo stesso) per darti la gerarchia, puoi usarlo.

Per il tuo Q3 su PUT con informazioni complete, puoi contrassegnare i campi che sono stati aggiornati e inviare al server ulteriori informazioni sui metadati per poterli analizzare e aggiornare da soli.

    
risposta data 19.01.2016 - 19:55
fonte

Leggi altre domande sui tag