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


208

Я пишу сервис 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

...

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

POST /customer/123?cmd=sendEmail

POST /cmd/sendEmail?customerId=123

GET /customer/count 

3
Чтобы ответить на ваш вопрос о «вызовах бизнес-логики», есть пост POSTот самого Роя Филдинга: roy.gbiv.com/untangled/2009/it-is-okay-to-use-post, где основная идея такова: если нет Это метод (например, GETили PUT), идеально подходящий для вашей операции использования POST.
Рохока

Это в значительной степени то, что я в итоге сделал. Выполните вызовы REST для получения и обновления известных ресурсов, используя GET, PUT, DELETE. POST для добавления новых ресурсов и POST с некоторым описательным URL для вызовов бизнес-логики.
magiconair

Что бы вы ни выбрали, если эта операция не является частью ответа GET, у вас нет службы RESTful. Я не вижу этого здесь
MStodd

Ответы:


69

У вас есть два варианта:

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

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


ОК, POST / 303 имеет для меня смысл. PATCH и MERGE Я не смог найти в списке действительных HTTP-глаголов, так что потребовалось бы дополнительное тестирование. Как мне создать URI, если я хочу, чтобы система отправила электронное письмо клиенту 123? Что-то вроде чистого вызова метода RPC, который вообще не меняет состояние объекта. Что такое RESTful способ сделать это?
magiconair

Я не понимаю вопрос URI электронной почты. Вы хотите внедрить шлюз, к которому вы можете POST отправлять электронное письмо, или вы ищете mailto: customer.123@service.org?
Ян Альгермиссен

15
Ни REST, ни HTTP не имеют ничего общего с CRUD, кроме некоторых людей, приравнивающих методы HTTP к CRUD. REST - это управление состоянием ресурса путем передачи представлений. Чего бы вы ни хотели достичь, вы делаете, передавая представление в ресурс с соответствующей семантикой. Остерегайтесь терминов «чистые вызовы методов» или «бизнес-логика», так как они слишком легко подразумевают «HTTP для транспорта». Если вам нужно отправить электронное письмо, POST на ресурс шлюза, если вам нужно объединить учетные записи, создать новое и POST-представления двух других и т. Д.
Jan Algermissen,

9
Смотрите также, как Google это делает: googlecode.blogspot.com/2010/03/…
Marius

4
williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot PATCH [{"op": "test", "path": "/ a / b / c", "value" : "foo"}, {"op": "remove", "path": "/ a / b / c"}, {"op": "add", "path": "/ a / b / c" , "value": ["foo", "bar"]}, {"op": "replace", "path": "/ a / b / c", "value": 42}, {"op": "move", "from": "/ a / b / c", "path": "/ a / b / d"}, {"op": "copy", "from": "/ a / b / d "," path ":" / a / b / e "}]
intotecho

48

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

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

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

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

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


1
@John Saunders POST не обязательно должен создавать новый ресурс, доступный через URI: tools.ietf.org/html/rfc2616#section-9.5
wsorenson

10
@wsorensen: я знаю, что это не должно приводить к появлению нового URL, но все же думал, что POST /customer/123должен создать очевидную вещь, которая логически соответствует потребителю 123. Может быть, заказ? PUT, /customer/123/statusкажется, имеет больше смысла, предполагая, что POST /customersнеявно создал status(и предполагая, что это законный REST).
Джон Сондерс

1
@John Saunders: на практике, если мы хотим обновить поле в ресурсе, расположенном по заданному URI, POST имеет больше смысла, чем PUT, и без UPDATE я считаю, что его часто используют в службах REST. POST для / клиентов может создать нового клиента, и статус PUT для / customer / 123 / может лучше соответствовать слову спецификации, но я не думаю, что есть причина не использовать POST для / customer / 123 обновляет поле - оно краткое, имеет смысл и не противоречит ничему в спецификации.
wsorenson

8
Разве запросы POST не должны быть идемпотентными? Разумеется, обновление записи идемпотентно и, следовательно, должно быть ПУТ вместо этого?
Мартин Андерссон

1
@MartinAndersson - POSTзапросы не должны быть неидемпотентными . И, как уже упоминалось, PUTдолжен заменить весь ресурс.
Галле Кнаст

10

Вы должны использовать 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 лучше всего подходит для ваших бизнес-данных.

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

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


Обратите внимание, что между тем RFC для json-patch и xml-patch были завершены.
Botchniaque

8

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

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

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

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

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

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

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


2
Это было бы более полезно, если бы опубликовал в виде отдельного вопроса.
Intetecho

6

Для изменения статуса я думаю, что подход RESTful состоит в том, чтобы использовать логический подресурс, который описывает состояние ресурсов. Это IMO довольно полезно и чисто, когда у вас уменьшенный набор статусов. Это делает ваш 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

GET / клиент / 123 / неактивный должен вернуть 404

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

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

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

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

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

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

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

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

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

DELETE /customer/123/active

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


Как вы попадаете на подресурс?
MStodd

Я рефакторил ответ, пытаясь сделать его более понятным
raspacorp

5

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

отправка почты


POST /customers/123/mails

payload:
{from: x@x.com, subject: "foo", to: y@y.com}

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

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

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


GET /customers

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


Мне нравится способ логической группировки полей в подресурсе POST.
Гертас

3

Используйте 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);
}

2

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

Я считаю, что концепция 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.


2

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

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

правила :

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

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

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

Пример тестовых случаев, которые иллюстрируют приведенные выше правила (как видно в приложении к этому 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}}}

1

Проверьте http://www.odata.org/

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

MERGE /customer/123

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

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


Является MERGEдействительным глагол HTTP?
Джон Сондерс

3
Посмотрите на PATCH - это скоро станет стандартным HTTP и делает то же самое.
Ян Альгермиссен

@ Джон Сондерс Да, это метод расширения.
Макс Торо

К вашему сведению, MERGE был удален из OData v4. MERGE was used to do PATCH before PATCH existed. Now that we have PATCH, we no longer need MERGE. См. Docs.oasis-open.org/odata/new-in-odata/v4.0/cn01/…
tanguy_k

0

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

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

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

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

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

"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/

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.