RESTFul: azioni che cambiano lo stato


60

Sto progettando di costruire l'API RESTfull ma ci sono alcune domande di architettura che mi stanno creando dei problemi. L'aggiunta della logica di business back-end ai client è un'opzione che vorrei evitare poiché l'aggiornamento di più piattaforme client è difficile da mantenere in tempo reale quando la logica di business può cambiare rapidamente.

Diciamo che abbiamo l'articolo come risorsa (api / articolo), come dovremmo implementare azioni come pubblicare, non pubblicare, attivare o disattivare e così via, ma per cercare di renderlo il più semplice possibile?

1) Dovremmo usare api / article / {id} / {action} poiché può succedere molta logica di backend come spingere verso posizioni remote o cambiare più proprietà. Probabilmente la cosa più difficile qui è che dobbiamo inviare tutti i dati dell'articolo all'API per l'aggiornamento e non è stato possibile implementare il lavoro multiutente. Ad esempio, l'editor potrebbe inviare dati più vecchi di 5 secondi e sovrascrivere la correzione che qualche altro giornalista ha appena fatto 2 secondi fa e non c'è modo di spiegarlo ai clienti poiché coloro che pubblicano un articolo non sono in alcun modo collegati all'aggiornamento del contenuto.

2) La creazione di una nuova risorsa può anche essere un'opzione, api / article- {action} / id, ma la risorsa restituita non sarebbe un articolo- {action} ma un articolo che non sono sicuro che sia corretto. Anche nella classe di articoli in codice lato server sta gestendo tutto il lavoro su entrambe le risorse e non sono sicuro che ciò vada contro il pensiero di RESTfull

Eventuali suggerimenti sono i benvenuti ..


È perfettamente legale che le "azioni" facciano parte di un URI RESTful - se indicano un'azione / algoritmo da eseguire. Cosa c'è che non va api/article?action=publish? I parametri di query sono destinati a tali casi in cui lo stato della risorsa dipende dall'algoritmo (o dall'azione) di cui si parla. Ad esempio api/articles?sort=ascè valido
PhD

1
Ti suggerisco di dare un'occhiata a questo articolo , che potrebbe ispirarti con una soluzione ancora più RESTful.
Benjol,

Uno dei problemi che vedo con api / article? Action = publishing è che nell'applicazione RESTfull dovrebbe inviare TUTTI i dati dell'articolo per la pubblicazione mentre preferirei fare proprio questo: api / article / 4545 /
publishing

2
Ottima risorsa su questo e altro: REST API Design - Resource Modeling
Izhaki,

@PhD: sebbene sia perfettamente legale nel protocollo, gli URI generalmente dovrebbero essere nomi piuttosto che verbi. Avere un verbo nell'URI è generalmente un segno di cattiva progettazione REST.
Sdraiati Ryan il

Risposte:


49

Trovo che le pratiche descritte qui siano utili:

Che dire delle azioni che non rientrano nel mondo delle operazioni CRUD?

Qui è dove le cose possono diventare confuse. Esistono diversi approcci:

  1. Ristruttura l'azione in modo che appaia come un campo di una risorsa. Funziona se l'azione non accetta parametri. Ad esempio un'azione di attivazione potrebbe essere mappata su un activatedcampo booleano e aggiornata tramite PATCH alla risorsa.
  2. Trattalo come una sotto-risorsa con principi RESTful. Ad esempio, le API di GitHub consente di stella à Gist con PUT /gists/:id/stare Rimuovi da speciali con DELETE /gists/:id/star.
  3. A volte non hai davvero modo di mappare l'azione su una struttura RESTful sensibile. Ad esempio, una ricerca multi-risorsa non ha davvero senso essere applicata all'endpoint di una risorsa specifica. In questo caso, /searchavrebbe più senso anche se non è una risorsa. Questo è OK: fai ciò che è giusto dal punto di vista del consumatore API e assicurati che sia chiaramente documentato per evitare confusione.

Io voto per l'approccio 2. Sebbene possa sembrare una maldestra risorse artificiali per i chiamanti API, in realtà non hanno alcuna conoscenza di ciò che sta accadendo sul server. Se chiamo POST /article/123/deactivationsper creare una nuova richiesta di disattivazione per l'articolo 123, il server potrebbe non solo disattivare la risorsa richiesta, ma in realtà memorizzare la mia richiesta di disattivazione in modo da poter recuperare il suo stato in un secondo momento.
JustAMartin,

2
perchè PUT /gists/:id/star no POST /gists/:id/star?
Filip Bartuzi,

10
@FilipBartuzi Poiché PUT è idempotente, ovvero, indipendentemente da quante volte esegui un'azione con gli stessi parametri, il risultato è sempre lo stesso (ad esempio, se accendi una luce da spento a acceso, cambia. Se provi a girare di nuovo acceso, non cambia nulla - è già acceso). Il POST non è idempotente, ovvero ogni volta che si esegue un'azione, anche con gli stessi parametri, l'azione ha un risultato diverso (ad es. Se si invia una lettera a qualcuno, quella persona riceve una lettera. Se si invia una lettera identica a la stessa persona, ora hanno 2 lettere).
Raffaello

9

Le operazioni che comportano importanti cambiamenti di stato e comportamento sul lato server come l'azione "pubblica" descritta sono difficili da modellare esplicitamente in REST. Una soluzione che vedo spesso è guidare implicitamente comportamenti così complessi attraverso i dati.

Prendi in considerazione l'ordine di merci tramite un'API REST esposta da un commerciante online. L'ordinamento è un'operazione complessa. Diversi prodotti verranno imballati e spediti, il tuo account verrà addebitato e riceverai una ricevuta. Puoi annullare il tuo ordine per un periodo di tempo limitato e c'è ovviamente una garanzia di rimborso totale che ti consente di rispedire i prodotti per un rimborso.

Invece di un'operazione di acquisto complessa, tale API potrebbe consentire di creare una nuova risorsa, un ordine di acquisto. All'inizio puoi apportare qualsiasi modifica desideri: aggiungere o rimuovere prodotti, cambiare l'indirizzo di spedizione, scegliere un'altra opzione di pagamento o annullare del tutto il tuo ordine. Puoi fare tutto questo perché non hai ancora acquistato nulla, stai solo manipolando alcuni dati sul server.

Una volta completato l'ordine di acquisto e trascorso il periodo di prova, il server blocca l'ordine per impedire ulteriori modifiche. Solo in questo momento inizia la complessa sequenza di operazioni, ma non è possibile controllarla direttamente, solo indirettamente attraverso i dati inseriti in precedenza nell'ordine di acquisto.

Sulla base della tua descrizione, "pubblicare" potrebbe essere implementato in questo modo. Invece di esporre un'operazione, metti una copia della bozza che hai esaminato e desideri pubblicare come nuova risorsa sotto / pubblicare. Ciò garantisce che eventuali aggiornamenti successivi alla bozza non vengano pubblicati anche se l'operazione di pubblicazione stessa viene completata ore dopo.


L'idea di cambiare l'intera risorsa da articolo inedito a bozza si adatterebbe esattamente a questo caso, ma non si adatterebbe a tutte le altre azioni che esistono su una risorsa in generale. REST dovrebbe anche gestirlo? Forse lo sto abusando e dovrei usarlo solo come CRUD e niente di più, come le query SQL in cui non mi aspetto alcuna logica all'interno della query stessa.
Miro Svrtan,

Perché sto chiedendo tutto questo? Beh, da qualche tempo le app Web iniziano a essere multipiattaforma e preferirei mantenere molta logica di bussines sul server poiché l'aggiornamento della logica di bussiness su iOS, Android, web, desktop o qualsiasi altra piattaforma venga in mente sta diventando praticamente impossibile da fare rapidamente e vorrei evitare tutti i problemi di retrocompatibilità quando si cambia un piccolo pezzo di BL.
Miro Svrtan,

2
Penso che REST sia in grado di gestire bene la logica aziendale, ma non è adatto per esporre la logica aziendale esistente senza avere in mente REST. Questo è il motivo per cui molte aziende come Microsoft, SAP e altre spesso espongono solo dati con operazioni CRUD, proprio come hai detto tu. Dai un'occhiata all'Open data Protocol per vedere come lo fanno.
Ferenc Mihaly,

7

dobbiamo inviare tutti i dati dell'articolo all'API per l'aggiornamento e non è stato possibile implementare il lavoro multiutente. Ad esempio, l'editor potrebbe inviare dati più vecchi di 5 secondi e sovrascrivere la correzione che qualche altro giornalista ha appena fatto 2 secondi fa e non c'è modo che io possa spiegarlo ai clienti poiché coloro che pubblicano un articolo non sono in alcun modo collegati all'aggiornamento del contenuto.

Questo genere di cose è una sfida, qualunque cosa tu faccia, è un problema molto simile al controllo del codice sorgente distribuito (mercurial, git, ecc.) E la soluzione, scritta in HTTP / ReST, sembra un po 'simile.

Supponiamo che tu abbia due utenti, Alice e Bob, che stanno entrambi lavorando /articles/lunch. (per chiarezza, la risposta è in grassetto)

Innanzitutto, Alice crea l'articolo.

PUT /articles/lunch HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

301 Moved Permanently
Location: /articles/lunch/1 

Il server non ha creato una risorsa, in quanto non vi era alcuna "versione" allegata alla richiesta (presupponendo un identificatore di /articles/{id}/{version}. Per eseguire la creazione, Alice è stata reindirizzata all'URL dell'articolo / versione che creerà. L'utente di Alice l'agente applicherà nuovamente la richiesta al nuovo indirizzo.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

201 Created

E ora l'articolo è stato creato. poi Bob guarda l'articolo:

GET /articles/lunch HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

301 Moved Permanently
Location: /articles/lunch/1 

Bob guarda lì:

GET /articles/lunch/1 HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

200 Ok
Content-Type: text/plain

Hey Bob, what do you want for lunch today?

Decide di aggiungere la propria modifica.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

301 Moved Permanently
Location: /articles/lunch/2

Come con Alice, Bob viene reindirizzato a dove creerà una nuova versione.

PUT /articles/lunch/2 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

201 Created

Infine, Alice decide che vorrebbe aggiungere al suo articolo:

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?
I was thinking about getting Sushi.

409 Conflict
Location: /articles/lunch/3
Content-Type: text/diff

---/articles/lunch/2
+++/articles/lunch/3
@@ 1,2 1,2 @@
 Hey Bob, what do you want for lunch today?
-Does pizza sound good to you, Alice?
+I was thinking about getting Sushi.

Invece di essere reindirizzato come di consueto, al client viene restituito un codice di stato diverso 409, che indica ad Alice che la versione da cui stava tentando di eseguire il branch è già stata ramificata. Le nuove risorse sono state comunque create (come mostrato dall'intestazione Location) e le differenze tra i due sono state incluse nel corpo della risposta. Alice ora sa che la richiesta che ha appena fatto deve essere unita in qualche modo.


Tutto questo reindirizzamento è correlato alla semantica di PUT, che richiede che vengano create nuove risorse esattamente dove viene richiesta la riga di richiesta. questo potrebbe anche salvare un ciclo di richiesta usando POSTinvece, ma poi il numero di versione dovrebbe essere codificato nella richiesta da qualche altra magia, che mi è sembrata meno ovvia ai fini dell'illustrazione, ma probabilmente sarebbe comunque preferito in una vera API per ridurre al minimo i cicli di richiesta / risposta.


1
Il versoning non è stato un problema qui, l'ho appena detto come esempio di possibili problemi se si utilizza l'articolo come risorsa per l'azione di pubblicazione
Miro Svrtan,

3

Ecco un altro esempio che non riguarda il contenuto dei documenti, ma piuttosto lo stato transitorio. (Trovo il controllo delle versioni - dato che, in generale, ogni versione può essere una nuova risorsa - una specie di problema facile.)

Diciamo che voglio esporre un servizio in esecuzione su una macchina tramite un REST in modo che possa essere arrestato, avviato, riavviato e così via.

Qual è l'approccio più RESTful qui? POST / servizio? Comando = riavvio, ad esempio? O POST / servizio / stato con un corpo di, diciamo, "in esecuzione"?

Sarebbe bello codificare le migliori pratiche qui e se REST è l'approccio giusto per questo tipo di situazione.

In secondo luogo, supponiamo che voglia guidare alcune azioni da un servizio che non influisce sul suo stato, ma innesca un effetto collaterale. Ad esempio, un servizio mailer che invia un rapporto, creato al momento della chiamata, a un gruppo di indirizzi e-mail.

GET / report potrebbe essere un modo per ottenere una copia del report da solo; ma cosa succede se vogliamo spingere sul lato server ulteriori azioni come la posta elettronica, come ho detto sopra. O scrivendo in un database.

Questi casi danzano attorno alla divisione delle risorse e vedo i modi per gestirli in modo orientato al REST, ma francamente mi sembra un po 'un trucco per farlo. Forse la domanda chiave è se un'API REST dovrebbe supportare gli effetti collaterali in generale.


2

REST è orientato ai dati e come tali risorse funzionano meglio come "cose" e non azioni. La semantica implicita dei metodi http; OTTIENI, METTI, ELIMINA, ecc. Servono a rafforzare l'orientamento. POST ovviamente è l'eccezione.

Una risorsa può essere una miscela di dati, ad es. contenuto dell'articolo; e metadati ie. pubblicato, bloccato, revisione. Esistono molti altri modi possibili per suddividere i dati, ma è necessario esaminare innanzitutto l'aspetto del flusso di dati per determinare quello ottimale (se presente). Ad esempio, potrebbe essere che le revisioni debbano essere la propria risorsa in base all'articolo come suggerisce TokenMacGuy.

Per quanto riguarda l'implementazione, probabilmente farei qualcosa di simile a ciò che suggerisce TockenMacGuy. Aggiungerei anche campi di metadati sull'articolo, non sulla revisione, come "bloccato" e "pubblicato".


1

Non pensarlo come una manipolazione diretta dello stato dell'articolo. Invece, stai inserendo un ordine di modifica che richiede la creazione dell'articolo.

È possibile modellare l'inserimento di un ordine di modifica come la creazione di una nuova risorsa ordine di modifica (POST). Ci sono molti vantaggi. Ad esempio, è possibile specificare una data e un'ora future in cui l'articolo deve essere pubblicato come parte dell'ordine di modifica e lasciare che il server si preoccupi della modalità di implementazione.

Se la pubblicazione non è un processo istantaneo, non devi aspettare che finisca prima di tornare al client. Devi solo riconoscere che l'ordine di modifica è stato creato e restituire l'ID dell'ordine di modifica. È quindi possibile utilizzare l'URL corrispondente a tale ordine di modifica per condividere lo stato dell'ordine di modifica.

Un'intuizione chiave per me era riconoscere questa metafora dell'ordine di cambiamento è solo un altro modo per descrivere la programmazione orientata agli oggetti. Invece di risorse, chiamiamo quindi oggetti. Invece di cambiare gli ordini, li chiamiamo messaggi. Un modo per inviare un messaggio da A a B in OO è avere una chiamata un metodo su B. Un altro modo per farlo, in particolare quando A e B sono su computer diversi, è avere A creare un nuovo oggetto, M e inviarlo a B. REST semplicemente formalizza tale processo.


In realtà lo considererei più vicino al modello dell'attore che a OO. Considerando le richieste REST come Messaggi (quali sono) le allinea perfettamente al Sourcing degli Eventi per la gestione dello stato. REST divide ordinatamente le sue interazioni lungo le linee CQRS. I messaggi GET sono Query, POST, PUT, PATCH, DELETE e cadono nell'area di comando.
WillD

0

Se ti capisco correttamente, penso che quello che hai sia più un problema di determinazione delle "regole aziendali" che un problema tecnico.

Il fatto che un articolo possa essere sovrascritto potrebbe essere risolto introducendo livelli di autorizzazione in cui gli utenti senior possono sovrascrivere le versioni degli utenti junior, introducendo anche versioni e una colonna per acquisire lo stato dell'articolo (ad es. "In sviluppo", "finale" , ecc.), potresti superarlo. È inoltre possibile dare all'utente la possibilità di selezionare una determinata versione tramite una combinazione di tempo di invio e numero di versione.

In tutti i casi di cui sopra, il servizio deve implementare le regole aziendali impostate. Quindi potresti chiamare il servizio con i parametri: userid, articolo, versione, azione (dove la versione è facoltativa, anche in questo caso dipende dalle tue regole aziendali).


Non credo che questa sia una regola di bussiness ma strettamente tecnologica. L'idea con l'aggiunta della versione è valida per aiutare a sovrascrivere le regole ma non risolve ancora la situazione in cui l'aggiornamento del contenuto e la pubblicazione del contenuto non sono azioni correlate.
Miro Svrtan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.