API CRUD: come specificare quali campi aggiornare?


9

Supponiamo che tu abbia un qualche tipo di struttura di dati, che è persistente in un qualche tipo di database. Per semplicità, chiamiamo questa struttura di dati Person. Ora ti viene assegnato il compito di progettare un'API CRUD, che consente ad altre applicazioni di creare, leggere, aggiornare ed eliminare messaggi Person. Per semplicità, supponiamo che questa API sia accessibile tramite un qualche tipo di servizio web.

Per le parti C, R e D di CRUD, il design è semplice. Userò la notazione funzionale simile a C # - l'implementazione potrebbe essere SOAP, REST / JSON o qualcos'altro:

class Person {
    string Name;
    DateTime? DateOfBirth;
    ...
}

Identifier CreatePerson(Person);
Person GetPerson(Identifier);
void DeletePerson(Identifier);

Che dire dell'aggiornamento? La cosa naturale da fare sarebbe

void UpdatePerson(Identifier, Person);

ma come specificare quali campi Personaggiornare?


Soluzioni che ho potuto escogitare:

  • Potresti sempre richiedere il passaggio di una Persona completa , ovvero il cliente farebbe qualcosa del genere per aggiornare la data di nascita:

    p = GetPerson(id);
    p.DateOfBirth = ...;
    UpdatePerson(id, p);
    

    Tuttavia, ciò richiederebbe una sorta di coerenza transazionale o blocco tra Get e Update; in caso contrario, è possibile sovrascrivere alcune altre modifiche apportate in parallelo da un altro client. Ciò renderebbe l'API molto più complicata. Inoltre, è soggetto a errori, poiché il seguente pseudo-codice (presupponendo una lingua client con supporto JSON)

    UpdatePerson(id, { "DateOfBirth": "2015-01-01" });
    

    - che sembra corretto - non cambierebbe solo DateOfBirth ma ripristinerebbe anche tutti gli altri campi su null.

  • Potresti ignorare tutti i campi che lo sono null. Tuttavia, come faresti a fare la differenza tra non cambiarla DateOfBirth e cambiarla deliberatamente in null ?

  • Cambia la firma in void UpdatePerson(Identifier, Person, ListOfFieldNamesToUpdate).

  • Cambia la firma in void UpdatePerson(Identifier, ListOfFieldValuePairs).

  • Utilizzare alcune funzionalità del protocollo di trasmissione: ad esempio, è possibile ignorare tutti i campi non contenuti nella rappresentazione JSON della persona. Tuttavia, ciò di solito richiede di analizzare JSON da soli e di non essere in grado di utilizzare le funzionalità integrate della libreria (ad esempio WCF).

Nessuna delle soluzioni mi sembra davvero elegante. Sicuramente, questo è un problema comune, quindi qual è la soluzione delle migliori pratiche utilizzata da tutti?


Perché l'identificatore non fa parte della persona? Per le Personistanze di nuova creazione che non sono ancora persistenti e nel caso in cui l'identificatore venga deciso come parte del meccanismo di persistenza, lasciarlo a null. Per quanto riguarda la risposta, JPA utilizza un numero di versione; se leggi la versione 23, quando aggiorni l'articolo se la versione nel DB è 24 la scrittura fallisce.
SJuan76,

Consentire e comunicare sia PUTe PATCHmetodi. Durante l'utilizzo PATCH, è necessario sostituire solo le chiavi di invio, con PUTl'intero oggetto sostituito.
Lode,

Risposte:


8

Se non si tiene traccia delle modifiche come requisito su questo oggetto (ad es. "L'utente John ha cambiato nome e data di nascita"), sarebbe più semplice sostituire l'intero oggetto nel DB con uno che si riceve dal consumatore. Questo approccio comporterebbe un numero leggermente maggiore di dati inviati tramite cavo, ma si sta evitando di leggere prima dell'aggiornamento.

Se hai requisiti di tracciamento delle attività. Il tuo mondo è molto più complicato e dovrai progettare come archiviare informazioni sulle azioni CRUD e su come intercettarle. Questo è il mondo in cui non vuoi immergerti se non hai tali requisiti.

Per quanto riguarda i valori prevalenti con transazioni separate, suggerirei di fare ricerche sul blocco ottimistico e pessimistico . Mitigano questo scenario comune:

  1. L'oggetto viene letto dall'utente1
  2. L'oggetto viene letto dall'utente2
  3. Oggetto scritto dall'utente1
  4. Oggetto scritto da user2 e sovrascritto da user1

Ogni utente ha una transazione diversa, quindi SQL standard con questo. Il più comune è il blocco ottimistico (citato anche da @ SJuan76 nel commento sulle versioni). La tua versione è il tuo record nel DB e durante la scrittura devi prima dare un'occhiata al DB se le versioni corrispondono. Se le versioni non corrispondono, sai che qualcuno ha aggiornato l'oggetto nel frattempo e devi rispondere con un messaggio di errore all'utente in merito a questa situazione. Sì, devi mostrare questa situazione all'utente.

Si noti che è necessario leggere il record effettivo dal DB prima di scriverlo (per un confronto ottimistico della versione di blocco), quindi l'implementazione della logica delta (scrivere solo valori modificati) potrebbe non richiedere ulteriori query di lettura prima di scrivere.

L'inserimento nella logica delta dipende fortemente dal contratto con il consumatore, ma si noti che il modo più semplice per il consumatore è costruire un payload completo anziché anche il delta.


2

Abbiamo un'API PHP al lavoro. Per gli aggiornamenti se un campo non viene inviato nell'oggetto JSON, viene impostato su NULL. Quindi passa tutto alla procedura memorizzata. La procedura memorizzata tenta di aggiornare ogni campo con field = IFNULL (input, campo). Quindi se solo 1 campo è nell'oggetto JSON, solo quel campo viene aggiornato. Per svuotare esplicitamente un campo impostato dobbiamo avere field = '', il DB quindi aggiorna il campo con una stringa vuota o il valore predefinito di quella colonna.


3
Come si imposta deliberatamente un campo su null che non è già null?
Robert Harvey,

Tutti i campi sono impostati NON NULL, quindi per impostazione predefinita i campi CHAR ottengono '' e tutti i campi interi ottengono 0.
Jared Bernacchi

1

Specificare l'elenco di campi aggiornato nella stringa di query.

PUT /resource/:id?fields=name,address,dob Body { //resource body }

Implementare unisci i dati memorizzati con il modello dal corpo della richiesta:

private ResourceModel MergeResourceModel(ResourceModel original, ResourceModel updated, List<string> fields)
{
    var comparer = new FieldComparer();

    foreach (
            var item in
            typeof (ResourceModel).GetProperties()
                    .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof (JsonIgnoreAttribute))))
    {
        if (fields.Contains(item.Name, comparer))
        {
            var property = typeof (ResourceModel).GetProperty(item.Name);
            property.SetValue(original, property.GetValue(updated));
        }
    }

    return original;
}
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.