CRUD API: Як вказати, які поля потрібно оновити?


9

Скажімо, у вас є якась структура даних, яка зберігається в якійсь базі даних. Для простоти назвемо цю структуру даних Person. Тепер вам покладено завдання створити API CRUD, який дозволяє іншим програмам створювати, читати, оновлювати та видаляти Persons. Для простоти припустимо, що до цього API можна отримати доступ через якусь веб-службу.

Для частин CRUD C, R і D конструкція проста. Я буду використовувати функцію C # -подобних позначень - реалізація може бути SOAP, REST / JSON або щось інше:

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

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

Що з оновленням? Природно, що було б зробити

void UpdatePerson(Identifier, Person);

але як би ви вказали, які поля Personоновити?


Рішення, які я міг би придумати:

  • Ви завжди можете вимагати передачі повної особи, тобто клієнт зробить щось подібне, щоб оновити дату народження:

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

    Однак це вимагає певної послідовності транзакцій або блокування між Get і Update; в іншому випадку ви можете перезаписати деякі інші зміни, зроблені паралельно іншим клієнтом. Це зробило б API набагато складнішим. Крім того, він схильний до помилок, оскільки наступний псевдо-код (припускаючи мову клієнта з підтримкою JSON)

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

    - що виглядає правильно - не лише змінить DateOfBirth, але й скине всі інші поля на нуль.

  • Ви можете ігнорувати всі поля, які є null. Однак як би ви змінили зміну, DateOfBirth а не навмисно змінювали її на нульову ?

  • Змініть підпис на void UpdatePerson(Identifier, Person, ListOfFieldNamesToUpdate).

  • Змініть підпис на void UpdatePerson(Identifier, ListOfFieldValuePairs).

  • Використовуйте деяку функцію протоколу передачі: Наприклад, ви можете ігнорувати всі поля, які не містяться в представленні JSON Особи. Однак для цього зазвичай потрібно самостійно проаналізувати JSON і не мати можливості використовувати вбудовані функції вашої бібліотеки (наприклад, WCF).

Жодне з рішень мені не здається справді елегантним. Безумовно, це поширена проблема, тож яке найкраще практичне рішення застосовують усі?


Чому ідентифікатор не є частиною особи? Для новостворених Personпримірників, які все ще не зберігаються, і якщо ідентифікатор вирішено як частина механізму збереження, просто залиште його на нульовому рівні. Що стосується відповіді, JPA використовує номер версії; якщо ви читаєте версію 23, коли ви оновлюєте елемент, якщо версія в БД становить 24, запис не вдається.
SJuan76

Дозволити та спілкуватися як із методами, так PUTі з ними PATCH. Під час використання PATCHслід замінювати лише ключі відправлення, при PUTцьому весь об'єкт замінюється.
Лоде

Відповіді:


8

Якщо у вас немає відстеження змін як вимоги щодо цього об’єкта (наприклад, "Користувач Джон змінив ім'я та дату народження"), найпростіше було б перекрити цілий об'єкт у БД на той, який ви отримаєте від споживача. Цей підхід передбачає дещо більше даних, що надсилаються за допомогою дроту, але ви уникаєте прочитати їх перед оновленням.

Якщо у вас є вимога відстеження активності Ваш світ набагато складніший, і вам потрібно буде розробити, як зберігати інформацію про дії CRUD та як їх перехоплювати. Це світ, у який ви не хочете занурюватися, якщо у вас немає такої вимоги.

Згідно з переважаючими значеннями за окремими транзакціями, я б запропонував провести дослідження навколо оптимістичного та песимістичного блокування . Вони пом'якшують цей загальний сценарій:

  1. Об'єкт читає користувач1
  2. Об'єкт читає user2
  3. Об'єкт написаний користувачем1
  4. Об'єкт написаний користувачем2 та перезаписані зміни користувачем1

Кожен користувач має різні транзакції, тому стандартний SQL з цим. Найбільш поширеним є оптимістичне блокування (також згадується @ SJuan76 у коментарі про версії). Ваша версія записується в БД і під час запису спочатку розглядаєте БД, якщо версії відповідають. Якщо версії не збігаються, ви знаєте, що хтось тим часом оновив об’єкт, і вам потрібно відповісти повідомленням про помилку споживачеві про цю ситуацію. Так, ви повинні показати цю ситуацію користувачеві.

Зауважте, що вам потрібно прочитати фактичну запис з БД перед її написанням (для оптимістичного порівняння версій блокування), тому реалізація дельта-логіки (записуйте лише змінені значення) може не вимагати додаткового запиту читання перед записом.

Введення в логіку дельти сильно залежить від договору зі споживачем, але зауважте, що найпростішим для споживача є також побудова повного корисного навантаження замість дельти.


2

У нас працює PHP API. Для оновлень, якщо поле не надсилається в об'єкт JSON, воно встановлюється на NULL. Потім він передає все до збереженої процедури. Збережена процедура намагається оновити кожне поле за допомогою поля = IFNULL (вхід, поле). Отже, якщо в об’єкті JSON лише 1 поле, то це поле оновлюється. Щоб явно спорожнити задане поле, у нас повинно бути поле = '', тоді БД оновлює поле або пустим рядком, або значенням за замовчуванням цього стовпця.


3
Як ви навмисно встановлюєте поле для нуля, яке вже не є нульовим?
Роберт Харві

Усі поля встановлені НЕ НУЛЬНО, тому за замовчуванням поля CHAR отримують '', а всі цілі поля отримують 0.
Джаред Бернакчі

1

Вкажіть оновлений список полів у рядку запитів.

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

Реалізація об'єднання збережених даних із моделлю від органу запиту:

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;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.