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