CRUD API: как указать, какие поля обновлять?

9

Допустим, у вас есть какая-то структура данных, которая сохраняется в какой-то базе данных. Для простоты назовем эту структуру данных Person. Теперь перед вами стоит задача разработать CRUD API, который позволяет другим приложениям создавать, читать, обновлять и удалять Personфайлы. Для простоты предположим, что этот API доступен через какой-то веб-сервис.

Для C, R и D частей CRUD дизайн прост. Я буду использовать 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.

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

  • Измените подпись на void UpdatePerson(Identifier, Person, ListOfFieldNamesToUpdate).

  • Измените подпись на void UpdatePerson(Identifier, ListOfFieldValuePairs).

  • Используйте некоторые возможности протокола передачи: например, вы можете игнорировать все поля, не содержащиеся в JSON-представлении Person. Однако для этого обычно требуется анализировать JSON самостоятельно и не иметь возможности использовать встроенные функции вашей библиотеки (например, WCF).

Ни одно из решений не кажется мне действительно элегантным. Конечно, это обычная проблема, так что же такое решение для всех?

Heinzi
источник
Почему идентификатор не является частью личности? Для вновь созданных Personэкземпляров, которые все еще не сохранены, и в случае, если идентификатор определен как часть механизма сохранения, просто оставьте его равным нулю. Что касается ответа, JPA использует номер версии; если вы читаете версию 23, при обновлении элемента, если версия в БД - 24, запись завершается неудачно.
SJuan76
Разрешить и общаться как PUTи PATCHметоды. При использовании PATCHследует заменять только ключи отправки, при PUTэтом весь объект заменяется.
Лоде

Ответы:

8

Если у вас нет отслеживания изменений в качестве требования к этому объекту (например, «Пользователь Джон изменил имя и дату рождения»), проще всего было бы переопределить весь объект в БД тем, который вы получили от потребителя. Этот подход будет включать немного больше данных, передаваемых по проводам, но вы избегаете чтения перед обновлением.

Если у вас есть требование отслеживания активности. Ваш мир намного сложнее, и вам нужно будет спроектировать, как хранить информацию о действиях CRUD и как их перехватывать. Это мир, в который вы не хотите погружаться, если у вас нет таких требований.

Что касается переопределения значений отдельными транзакциями, я бы предложил провести исследование вокруг оптимистической и пессимистической блокировки . Они смягчают этот общий сценарий:

  1. Объект читается пользователем1
  2. Объект читается user2
  3. Объект написан user1
  4. Объект, написанный user2 и перезаписанный изменениями user1

У каждого пользователя разные транзакции, поэтому стандартный SQL с этим. Наиболее распространенной является оптимистическая блокировка (также упоминается @ SJuan76 в комментарии о версиях). Ваша версия, ваша запись в БД и во время записи вы сначала заглядываете в БД, если версии совпадают. Если версии не совпадают, вы знаете, что кто-то обновил объект тем временем, и вам нужно ответить об этом потребителю сообщением об ошибке. Да, вы должны показать эту ситуацию пользователю.

Обратите внимание, что перед записью необходимо прочитать фактическую запись из БД (для сравнения версий с оптимистической блокировкой), поэтому реализация дельта-логики (запись только измененных значений) может не потребовать дополнительного запроса на чтение перед записью.

Использование дельта-логики сильно зависит от контракта с потребителем, но обратите внимание, что для потребителя проще всего построить полную полезную нагрузку вместо дельты.

luboskrnac
источник
2

У нас есть PHP API на работе. Для обновлений, если поле не отправлено в объекте JSON, ему присваивается значение NULL. Затем он передает все хранимой процедуре. Хранимая процедура пытается обновить каждое поле с помощью field = IFNULL (input, field). Поэтому, если в объекте JSON находится только 1 поле, обновляется только это поле. Чтобы явно очистить поле набора, мы должны иметь поле = '', затем БД обновит поле либо пустой строкой, либо значением этого столбца по умолчанию.

Джаред Бернакки
источник
3
Как вы намеренно устанавливаете поле в нуль, которое еще не равно нулю?
Роберт Харви
Все поля установлены как NOT NULL, поэтому по умолчанию поля 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;
}
adisembiring
источник