CRUD API: Jak określić, które pola należy zaktualizować?

9

Załóżmy, że masz jakąś strukturę danych, która jest utrwalona w jakiejś bazie danych. Dla uproszczenia nazwijmy tę strukturę danych Person. Masz teraz zadanie zaprojektowania interfejsu CRUD API, który pozwala innym aplikacjom tworzyć, czytać, aktualizować i usuwać Persons. Dla uproszczenia załóżmy, że dostęp do tego interfejsu API można uzyskać za pośrednictwem usługi internetowej.

W przypadku części CRUD C, R i D konstrukcja jest prosta. Użyję notacji funkcjonalnej podobnej do C # - implementacją może być SOAP, REST / JSON lub coś innego:

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

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

Co z aktualizacją? Naturalną rzeczą do zrobienia byłoby

void UpdatePerson(Identifier, Person);

ale jak określisz, które pola Personchcesz zaktualizować?


Rozwiązania, które mógłbym wymyślić:

  • Zawsze możesz wymagać przekazania pełnej Osoby, tzn. Klient zrobiłby coś takiego, aby zaktualizować datę urodzenia:

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

    Wymagałoby to jednak pewnego rodzaju spójności transakcyjnej lub blokowania między Get a Update; w przeciwnym razie możesz zastąpić inną zmianę wykonaną równolegle przez innego klienta. Sprawiłoby to, że interfejs API byłby znacznie bardziej skomplikowany. Ponadto jest podatny na błędy, ponieważ następujący pseudo-kod (zakładając język klienta z obsługą JSON)

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

    - który wygląda poprawnie - nie tylko zmieni DateOfBirth, ale także zresetuje wszystkie inne pola do null.

  • Możesz zignorować wszystkie pola, które są null. Jak jednak zrobiłbyś różnicę między niezmienianiem DateOfBirth a celowym zmienianiem go na zero ?

  • Zmień podpis na void UpdatePerson(Identifier, Person, ListOfFieldNamesToUpdate).

  • Zmień podpis na void UpdatePerson(Identifier, ListOfFieldValuePairs).

  • Użyj pewnej funkcji protokołu transmisji: na przykład możesz zignorować wszystkie pola nie zawarte w reprezentacji JSON osoby. Zwykle wymaga to jednak samodzielnego przeanalizowania JSON i niemożności korzystania z wbudowanych funkcji biblioteki (np. WCF).

Żadne z rozwiązań nie wydaje mi się naprawdę eleganckie. Z pewnością jest to powszechny problem, więc jakie jest najlepsze rozwiązanie stosowane przez wszystkich?

Heinzi
źródło
Dlaczego identyfikator nie jest częścią osoby? W przypadku nowo utworzonych Personinstancji, które nadal nie są utrwalane, a jeśli identyfikator jest ustalany jako część mechanizmu trwałości, pozostaw go zerowy. Jeśli chodzi o odpowiedź, JPA używa numeru wersji; jeśli czytasz wersję 23, po zaktualizowaniu elementu, jeśli wersja w DB to 24, zapis nie powiedzie się.
SJuan76,
Zezwalaj i komunikuj obie metody PUTi PATCHmetody. Podczas korzystania PATCHnależy wymieniać tylko klucze wysyłania, zastępując PUTcały obiekt.
Lode,

Odpowiedzi:

8

Jeśli nie masz śledzenia zmian jako wymaganego dla tego obiektu (np. „Użytkownik Jan zmienił imię i datę urodzenia”), najprostszym byłoby zastąpienie całego obiektu w DB tym, który otrzymałeś od konsumenta. Takie podejście wymagałoby nieco większej ilości danych przesyłanych przewodowo, ale unika się odczytu przed aktualizacją.

Jeśli masz wymóg śledzenia aktywności. Twój świat jest znacznie bardziej skomplikowany i musisz zaprojektować, jak przechowywać informacje o działaniach CRUD i jak je przechwytywać. To świat, w który nie chcesz nurkować, jeśli nie masz takich wymagań.

Jeśli chodzi o nadpisywanie wartości oddzielnymi transakcjami, sugerowałbym przeprowadzenie badań dotyczących optymistycznego i pesymistycznego blokowania . Łagodzą ten typowy scenariusz:

  1. Obiekt jest odczytywany przez użytkownika 1
  2. Obiekt jest odczytywany przez użytkownika2
  3. Obiekt napisany przez użytkownika 1
  4. Obiekt napisany przez użytkownika 2 i zastąpione zmiany przez użytkownika 1

Każdy użytkownik ma inną transakcję, dlatego też standardowy SQL. Najczęstszym jest optymistyczne blokowanie (wspomniane również przez @ SJuan76 w komentarzu na temat wersji). Twoja wersja jest Twoim rekordem w DB, a podczas pisania najpierw sprawdzasz DB, jeśli wersje się zgadzają. Jeśli wersje się nie zgadzają, wiesz, że w międzyczasie ktoś zaktualizował obiekt i musisz odpowiedzieć komunikatem o błędzie dla konsumenta na temat tej sytuacji. Tak, musisz pokazać tę sytuację użytkownikowi.

Zauważ, że musisz odczytać aktualny rekord z DB przed jego zapisaniem (dla optymistycznego porównania wersji blokowania), więc implementacja logiki delta (tylko zmienione wartości zapisu) może nie wymagać dodatkowego zapytania odczytu przed zapisem.

Wprowadzenie logiki delta w dużym stopniu zależy od umowy z konsumentem, ale zauważ, że najłatwiej dla konsumenta jest zbudowanie pełnego ładunku zamiast delty.

luboskrnac
źródło
2

Mamy działający interfejs API PHP. W przypadku aktualizacji, jeśli pole nie jest wysyłane w obiekcie JSON, zostaje ustawione na NULL. Następnie przekazuje wszystko do procedury składowanej. Procedura składowana próbuje zaktualizować każde pole za pomocą pola = IFNULL (dane wejściowe, pole). Więc jeśli tylko 1 pole znajduje się w obiekcie JSON, tylko to pole jest aktualizowane. Aby jawnie opróżnić ustawione pole, musimy mieć pole = '', DB następnie aktualizuje pole pustym łańcuchem lub wartością domyślną tej kolumny.

Jared Bernacchi
źródło
3
Jak celowo ustawić pole na null, które nie jest już puste?
Robert Harvey,
Wszystkie pola są ustawione NIE NULL, więc domyślnie pola CHAR otrzymują „”, a wszystkie pola liczb całkowitych otrzymują 0.
Jared Bernacchi,
1

Podaj zaktualizowaną listę pól w ciągu zapytania.

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

Zaimplementuj scalanie przechowywanych danych z modelem z treści żądania:

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;
}
adisembiring
źródło