Найкраща практика для часткових оновлень у сервісі 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

...

Оновлення : для розширення питання. Як можна включити "бізнес-логічні дзвінки" в програм 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.
rojoca

Це майже все, що я закінчив робити. Здійснюйте REST-дзвінки для отримання та оновлення відомих ресурсів за допомогою GET, PUT, DELETE. POST для додавання нових ресурсів та POST з деякою описовою URL-адресою для бізнес-логічних викликів.
магіконер

Що б ви не вирішили, якщо ця операція не є частиною відповіді GET, у вас немає служби RESTful. Я тут цього не бачу
MStodd

Відповіді:


69

У вас є два варіанти:

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

  2. Використовуйте POSTдля підресурсу і поверніться 303 Див. Інше, заголовок Location вказує на основний ресурс. Намір 303 полягає в тому, щоб сказати клієнту: "Я виконав ваш POST, і наслідком було те, що якийсь інший ресурс було оновлено. Дивіться заголовок розташування, для якого ресурсу це було". POST / 303 призначений для ітеративних доповнень до ресурсів для створення стану якогось основного ресурсу, і він ідеально підходить для часткових оновлень.


Гаразд, POST / 303 має сенс для мене. PATCH і MERGE Я не зміг знайти в списку дійсних дієслів HTTP, щоб вимагати додаткового тестування. Як я можу побудувати URI, якщо хочу, щоб система надсилала електронному листу клієнту 123? Щось на зразок чистого виклику методу RPC, який зовсім не змінює стан об'єкта. Який РЕШИЙ спосіб зробити це?
магіконер

Я не розумію питання щодо 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": "тест", "шлях": "/ a / b / c", "значення" : "foo"}, {"op": "видалити", "шлях": "/ a / b / c"}, {"op": "додати", "шлях": "/ a / b / c" , "value": ["foo", "bar"]}, {"op": "замінити", "path": "/ a / b / c", "value": 42}, {"op": "переміщення", "від": "/ a / b / c", "шлях": "/ a / b / d"}, {"op": "копіювати", "з": "/ a / b / d "," path ":" / a / b / e "}]
інтохо

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 to / customer / 123 / може краще узгоджуватися зі словом специфікації, але що стосується передового досвіду, я не думаю, що немає причин не POST в / customer / 123 для оновлення поля - це стисло, має сенс і не суворо суперечить чомусь із специфікації.
wsorenson

8
Чи не повинні запити POST не бути ідентичними? Безумовно, оновлення запису неправдоподібне і, таким чином, повинно бути PUT замість цього?
Мартін Андерссон

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 найкраще підходить для ваших бізнес-даних.

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
Це було б корисніше, якби воно було окреме питання.
інтохо

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
}

Ви створюєте концептуальний субресурс для свого клієнтського ресурсу. Це також відповідає визначенню ресурсу Роя Філдінга: "... Ресурс - це концептуальне відображення набору сутностей, а не сутність, яка відповідає відображенню в будь-який конкретний момент часу ..." У цьому випадку концептуальне відображення є активним-замовник до замовника зі статусом = АКТИВНИЙ.

Операція зчитування:

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

Якщо ви робите ці дзвінки відразу після того, як один з них повинен повернути статус 404, успішний вихід може не включати статус, оскільки він неявний. Звичайно, ви все одно можете скористатися GET / customer / 123? Status = АКТИВНІ | НЕАКТИВНІ для прямого запиту клієнтського ресурсу.

Операція DELETE цікава, оскільки семантика може бути заплутаною. Але у вас є можливість не публікувати цю операцію для цього концептуального ресурсу або використовувати її відповідно до вашої логіки бізнесу.

DELETE /customer/123/active

Це може перенести вашого клієнта до статусу ВІДКЛЮЧЕНО / ВИМОЖЕНО або до протилежного (АКТИВНО / НЕАКТИВНО).


Як потрапити на підресурс?
MStodd

Я відремонтував відповідь, намагаючись зробити це більш зрозумілим
raspacorp

5

Що потрібно додати до розширеного запитання. Я думаю, що часто можна ідеально спроектувати складніші ділові дії. Але вам слід віддати метод / процедуру стилю мислення та більше думати про ресурси та дієслова.

відправлення пошти


POST /customers/123/mails

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

Потім реалізація цього ресурсу + POST надсилатиме пошту. при необхідності ви можете запропонувати щось на кшталт / customer / 123 / outbox, а потім запропонувати посилання на ресурси на / customer / mail / {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 на цільовий ресурс із застосуванням мультимедійного типу application / 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 і робить те саме.
Ян Альгерміссен

@John Saunders Так, це метод розширення.
Макс Торо

FYI 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.