Лучшая практика для частичных обновлений службы RESTful

Я пишу службу RESTful для системы управления клиентами, и я пытаюсь найти оптимальную практику для обновления записей частично. Например, я хочу, чтобы вызывающий мог прочитать полную запись с запросом GET. Но для его обновления допускаются только определенные операции над записью, такие как изменение статуса из ENABLED в DISABLED. (У меня более сложные сценарии, чем это)

Я не хочу, чтобы вызывающий абонент отправлял всю запись только с обновленным полем по соображениям безопасности (он также чувствует себя излишним).

Есть ли рекомендуемый способ создания URI? При чтении REST-книг вызовы стиля RPC кажутся неодобрительными.

Если следующий вызов возвращает полную запись клиента для клиента с идентификатором 123

GET /customer/123
<customer>
    {lots of attributes}
    <status>ENABLED</status>
    {even more attributes}
</customer>

как мне обновить статус?

POST /customer/123/status
<status>DISABLED</status>

POST /customer/123/changeStatus
DISABLED

...

Обновить. Чтобы увеличить вопрос. Как включить "бизнес-логические вызовы" в REST api? Есть ли согласованный способ сделать это? Не все методы CRUD по своей природе. Некоторые из них более сложны, например "sendEmailToCustomer (123)", "mergeCustomers (123, 456)", "countCustomers()"

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

Спасибо Франк

Ответ 1

В основном у вас есть два варианта:

  • Используйте PATCH (но обратите внимание, что вы должны определить свой собственный тип носителя, который указывает, что будет точно)

  • Используйте POST для дополнительного ресурса и возвращайте 303 См. раздел "Другое" с заголовком "Местоположение", указывающим на основной ресурс. Цель 303 заключается в том, чтобы сообщить клиенту: "Я выполнил ваш POST, и эффект состоял в том, что был обновлен какой-то другой ресурс. См. Заголовок местоположения, для которого был ресурс". POST/303 предназначен для итеративных дополнений к ресурсам для создания состояния некоторого основного ресурса, и он идеально подходит для частичных обновлений.

Ответ 2

Вы должны использовать POST для частичных обновлений.

Чтобы обновить поля для клиента 123, сделайте POST/клиенту/123.

Если вы хотите обновить только статус, вы также можете указать PUT/customer/123/status.

Обычно запросы GET не должны иметь побочных эффектов, а PUT - для записи/замены всего ресурса.

Это следует непосредственно из HTTP, как показано здесь: http://en.wikipedia.org/wiki/HTTP_PUT#Request_methods

Ответ 3

Вы должны использовать PATCH для частичных обновлений - либо с помощью json-patch документов (см. http://tools.ietf.org/html/draft-ietf-appsawg-json-patch-08, либо http://www.mnot.net/blog/2012/09/05/patch) или фреймворк XML-патча (см. http://tools.ietf.org/html/rfc5261). На мой взгляд, json-patch лучше всего подходит для ваших бизнес-данных.

PATCH с документами патчей JSON/XML имеет очень простую семантику для частичных обновлений. Если вы начнете использовать POST с измененными копиями исходного документа, для частичных обновлений вы в скором времени столкнетесь с проблемами, когда вы хотите, чтобы отсутствующие значения (или, вернее, нулевые значения) представляли либо "игнорировать это свойство", либо "устанавливали это свойство в пустое значение" - и это ведет к кроличьей дыре взломанных решений, что в итоге приведет к вашему формату патча.

Здесь вы можете найти более подробный ответ: http://soabits.blogspot.dk/2013/01/http-put-patch-or-post-partial-updates.html.

Ответ 4

У меня возникает аналогичная проблема. Кажется, что PUT на субресурсе работает, когда вы хотите обновить только одно поле. Однако иногда вы хотите обновить кучу вещей: подумайте о веб-форме, представляющей ресурс, с возможностью изменения некоторых записей. Пользовательская подача формы не должна приводить к множественным PUT.

Вот два решения, о которых я могу думать:

  • выполните PUT со всем ресурсом. На стороне сервера определите семантику, что PUT со всем ресурсом игнорирует все значения, которые не изменились.

  • выполните PUT с частичным ресурсом. На стороне сервера определите семантику этого объединения.

2 - это просто оптимизация полосы пропускания 1. Иногда 1 является единственным вариантом, если ресурс определяет некоторые поля, требуемые поля (думаю, прото-буферы).

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

Комментарии?

Ответ 5

Что добавить к вашему расширенному вопросу. Я думаю, вы часто можете идеально разрабатывать более сложные бизнес-действия. Но вы должны отдать стиль метода/процедуры мышления и больше думать о ресурсах и глаголах.

почтовые отправления


POST /customers/123/mails

payload:
{from: [email protected], subject: "foo", to: [email protected]}

Реализация этого ресурса + POST затем отправит почту. при необходимости вы можете предложить что-то вроде /customer/ 123/outbox, а затем предлагать ссылки ресурсов на /customer/mails/ {mailId}.

количество клиентов

Вы можете обрабатывать его как ресурс поиска (включая метаданные поиска с поисковой страницей и числовой информацией, что дает вам количество клиентов).


GET /customers

response payload:
{numFound: 1234, paging: {self:..., next:..., previous:...} customer: { ...} ....}

Ответ 6

Для изменения статуса я считаю, что подход RESTful заключается в использовании логического под-ресурса, который описывает состояние ресурсов. Эта ИМО очень полезна и чиста, когда у вас есть уменьшенный набор статусов. Это делает ваш API более выразительным, не заставляя существующие операции для вашего ресурса клиента.

Пример:

POST /customer/active  <-- Providing entity in the body a new customer
{
  ...  // attributes here except status
}

Служба POST должна вернуть вновь созданного клиента с идентификатором:

{
    id:123,
    ...  // the other fields here
}

GET для созданного ресурса будет использовать местоположение ресурса:

GET /customer/123/active

A GET/customer/123/inactive должен вернуть 404

Для операции PUT без предоставления Json-объекта он просто обновит статус

PUT /customer/123/inactive  <-- Deactivating an existing customer

Предоставление сущности позволит вам обновить содержимое клиента и обновить статус одновременно.

PUT /customer/123/inactive
{
    ...  // entity fields here except id and status
}

Вы создаете концептуальный под-ресурс для своего ресурса клиента. Это также согласуется с определением ресурса Роя Филдинга: "... Ресурс представляет собой концептуальное сопоставление с набором объектов, а не с сущностью, которая соответствует отображению в любой конкретный момент времени..." В этом случае концептуальное отображение активно - клиент к клиенту со статусом = АКТИВ.

Операция чтения:

GET /customer/123/active 
GET /customer/123/inactive

Если вы делаете эти вызовы сразу после того, как один из них должен вернуть статус 404, успешный вывод может не включать статус, поскольку он неявный. Конечно, вы можете использовать GET/customer/123? Status = ACTIVE | INACTIVE для непосредственного запроса ресурса клиента.

Операция DELETE интересна тем, что семантика может вводить в заблуждение. Но у вас есть возможность не публиковать эту операцию для этого концептуального ресурса или использовать ее в соответствии с вашей бизнес-логикой.

DELETE /customer/123/active

Это может привести ваш клиент к статусу DELETED/DISABLED или к другому статусу (ACTIVE/INACTIVE).

Ответ 7

Используйте PUT для обновления неполного/частичного ресурса.

Вы можете принять jObject как параметр и проанализировать его значение для обновления ресурса.

Ниже приведена функция, которую вы можете использовать в качестве ссылки:

public IHttpActionResult Put(int id, JObject partialObject)
{
    Dictionary<string, string> dictionaryObject = new Dictionary<string, string>();

    foreach (JProperty property in json.Properties())
    {
        dictionaryObject.Add(property.Name.ToString(), property.Value.ToString());
    }

    int id = Convert.ToInt32(dictionaryObject["id"]);
    DateTime startTime = Convert.ToDateTime(orderInsert["AppointmentDateTime"]);            
    Boolean isGroup = Convert.ToBoolean(dictionaryObject["IsGroup"]);

    //Call function to update resource
    update(id, startTime, isGroup);

    return Ok(appointmentModelList);
}

Ответ 8

Отъезд http://www.odata.org/

Он определяет метод MERGE, поэтому в вашем случае это будет примерно так:

MERGE /customer/123

<customer>
   <status>DISABLED</status>
</customer>

Обновляется только свойство status, а остальные значения сохраняются.

Ответ 9

Относительно вашего обновления.

Концепция CRUD, я считаю, вызвала некоторую путаницу в отношении дизайна API. CRUD представляет собой общую концепцию низкого уровня для основных операций, выполняемых на данных, а HTTP-глаголы - это просто методы запроса (созданные 21 год назад), которые могут или не может отображаться на операцию CRUD. На самом деле, попробуйте найти присутствие сокращения CRUD в спецификации HTTP 1.0/1.1.

Очень хорошо объясненное руководство, в котором применяется прагматичное соглашение, можно найти в документации API облачной платформы Google. В нем описываются концепции создания API, основанного на ресурсах, который подчеркивает большой объем ресурсов над операциями и включает описанные вами варианты использования. Хотя это просто дизайн конвенции для их продукта, я думаю, что это имеет большой смысл.

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

В нем также говорится, что если операция не соответствует точно стандартным методам (List, Get, Create, Update, Delete в этом случае), можно использовать "Пользовательские методы" ", например BatchGet, который извлекает несколько объектов на основе ввода нескольких идентификаторов объекта или SendEmail.

Ответ 10

Это не имеет значения. Что касается REST, вы не можете сделать GET, потому что он не кэшируемый, но не имеет значения, используете ли вы POST или PATCH или PUT или что-то еще, и не имеет значения, как выглядит URL. Если вы выполняете REST, важно то, что, когда вы получаете представление своего ресурса с сервера, это представление может предоставить варианты перехода состояния клиента.

Если ваш ответ GET имеет переходы состояния, клиенту просто нужно знать, как их читать, и сервер может изменить их, если это необходимо. Здесь обновление выполняется с помощью POST, но если он был изменен на PATCH или изменился URL-адрес, клиент все еще знает, как сделать обновление:

{
  "customer" :
  {
  },
  "operations":
  [
    "update" : 
    {
      "method": "POST",
      "href": "https://server/customer/123/"
    }]
}

Вы можете зайти так далеко, чтобы перечислять требуемые/необязательные параметры, которые клиент может вернуть вам. Это зависит от приложения.

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

"email":
{
  "method": "POST",
  "href": "http://server/emailservice/send?customer=1234"
}

Некоторые хорошие видеоролики и пример архитектуры REST-презентатора - это. Stormpath использует только GET/POST/DELETE, что отлично, поскольку REST не имеет ничего общего с тем, какие операции вы используете или как должны выглядеть URL-адреса (кроме GET должны быть кэшируемыми):

https://www.youtube.com/watch?v=pspy1H6A3FM,
https://www.youtube.com/watch?v=5WXYw4J4QOU,
http://docs.stormpath.com/rest/quickstart/

Ответ 11

В RFC 7396: JSON Merge Patch (опубликованном через четыре года после публикации вопроса) описываются лучшие практики для PATCH с точки зрения формата и правил обработки.

В двух словах, вы отправляете HTTP PATCH целевому ресурсу с типом носителя MIME application/merge-patch + json и телом, представляющим только те части, которые вы хотите изменить/добавить/удалить, а затем следуйте приведенным ниже правилам обработки.

Правила:

  • Если предоставленное исправление слияния содержит элементы, которые не отображаются в целевом объекте, эти элементы добавляются.

  • Если цель содержит член, значение заменяется.

  • Нулевым значениям в патче слияния придают особое значение, чтобы указать удаление существующих значений в целевом объекте.

Пример тестовых случаев, иллюстрирующих приведенные выше правила (как видно из приложения этого RFC):

 ORIGINAL         PATCH           RESULT
--------------------------------------------
{"a":"b"}       {"a":"c"}       {"a":"c"}

{"a":"b"}       {"b":"c"}       {"a":"b",
                                 "b":"c"}
{"a":"b"}       {"a":null}      {}

{"a":"b",       {"a":null}      {"b":"c"}
"b":"c"}

{"a":["b"]}     {"a":"c"}       {"a":"c"}

{"a":"c"}       {"a":["b"]}     {"a":["b"]}

{"a": {         {"a": {         {"a": {
  "b": "c"}       "b": "d",       "b": "d"
}                 "c": null}      }
                }               }

{"a": [         {"a": [1]}      {"a": [1]}
  {"b":"c"}
 ]
}

["a","b"]       ["c","d"]       ["c","d"]

{"a":"b"}       ["c"]           ["c"]

{"a":"foo"}     null            null

{"a":"foo"}     "bar"           "bar"

{"e":null}      {"a":1}         {"e":null,
                                 "a":1}

[1,2]           {"a":"b",       {"a":"b"}
                 "c":null}

{}              {"a":            {"a":
                 {"bb":           {"bb":
                  {"ccc":          {}}}
                   null}}}