RESTful идемпотентность

Я разрабатываю веб-службу RESTful с использованием ROA (архитектура, ориентированная на ресурсы).

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

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

Шаг 1. Получите уникальный идентификатор транзакции для создания нового ресурса PERSON:::

**Client request:**
POST /CREATE_PERSON

**Server response:**
200 OK
transaction-id:"as8yfasiob"

Шаг 2. Создайте новый ресурс пользователя в запросе, который гарантированно будет уникальным, используя идентификатор транзакции:::

**Client request**
PUT /CREATE_PERSON/{transaction_id}
first_name="Big bubba"

**Server response**
201 Created             // (If the request is a duplicate, it would send this
PersonKey="398u4nsdf"   // same response without creating a new resource.  It
                        // would perhaps send an error response if the was used
                        // on a transaction id non-duplicate request, but I have
                        // control over the client, so I can guarantee that this
                        // won't happen)

Проблема, которую я вижу с этим подходом, заключается в том, что он требует отправки двух запросов на сервер, чтобы выполнить одну операцию создания нового ресурса PERSON. Это создает проблемы с производительностью, увеличивая вероятность того, что пользователь будет ждать, пока клиент выполнит свой запрос.

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

Есть ли способ сделать это?

Редактировать::::::

Решение, к которому мы пришли, заключалось в том, чтобы клиент получал UUID и отправлял его вместе с запросом. UUID — это очень большое число, занимающее пространство в 16 байт (2^128). Вопреки тому, что может интуитивно подумать человек с программным складом ума, принято генерировать UUID случайным образом и предполагать, что это уникальное значение. Это связано с тем, что количество возможных значений настолько велико, что вероятность случайного генерирования двух одинаковых чисел достаточно мала, чтобы быть практически невозможной.

Одно предостережение заключается в том, что наши клиенты запрашивают UUID с сервера (GET uuid/). Это связано с тем, что мы не можем гарантировать среду, в которой работает наш клиент. Если возникла проблема, например, с заполнением генератора случайных чисел на клиенте, то вполне мог возникнуть конфликт UUID.


person Chris Dutrow    schedule 02.06.2010    source источник


Ответы (4)


Вы используете неверный глагол HTTP для операции создания. RFC 2616 определяет семантику операций для POST и PUT.

Пункт 9.5:

Метод POST используется для запроса того, чтобы исходный сервер принял объект, включенный в запрос, в качестве нового подчиненного ресурса, идентифицированного Request-URI в строке запроса.

Пункт 9.6

PUT запрашивает, чтобы вложенный объект был сохранен под предоставленным Request-URI.

Есть тонкие детали этого поведения, например, PUT можно использовать для создания нового ресурса по указанному URL-адресу, если он еще не существует. Однако POST никогда не следует помещать новый объект в URL-адрес запроса, а PUT всегда должен помещать любой новый объект в URL-адрес запроса. Эта связь с URL-адресом запроса определяет POST как CREATE и PUT как UPDATE.

Согласно этой семантике, если вы хотите использовать PUT для создания нового человека, его следует создать в /CREATE_PERSON/{transaction_id}. Другими словами, идентификатор транзакции, возвращенный вашим первым запросом, должен быть ключом пользователя, используемым для извлечения этой записи позже. Вы не должны делать PUT запрос к URL-адресу, который не будет конечным местоположением этой записи.

Однако, еще лучше, вы можете сделать это как атомарную операцию, используя POST в /CREATE_PERSON. Это позволяет вам одним запросом создать запись о новом человеке и в ответ получить новый идентификатор (который также должен быть указан в заголовке HTTP Location).

Между тем, в рекомендациях REST указано, что глаголы не должны быть частью URL-адреса ресурса. Таким образом, URL-адрес для создания нового человека должен совпадать с адресом для получения списка всех лиц - /PERSONS (я предпочитаю форму множественного числа :-)).

Таким образом, ваш REST API становится:

  • получить все лица - GET /PERSONS
  • получить одинокого человека - GET /PERSONS/{id}
  • создать нового человека - POST /PERSONS с телом, содержащим данные для новой записи
  • обновить существующего человека или создать нового человека с известным идентификатором - PUT /PERSONS/{id} с телом, содержащим данные для обновленной записи.
  • удалить существующего человека - DELETE /PERSONS/{id}

Примечание. Я лично предпочитаю не использовать PUT для создания записей по двум причинам, если только мне не нужно создать подзапись с тем же идентификатором, что и уже существующая запись из другого набора данных (также известная как «внешний ключ бедняка»: -)).

Обновление: вы правы в том, что POST не является идемпотентным, и это соответствует спецификации HTTP. POST будет всегда возвращать новый ресурс. В приведенном выше примере этот новый ресурс будет контекстом транзакции.

Однако я хочу сказать, что вы хотите, чтобы PUT использовалось для создания нового ресурса (записи человека), и в соответствии со спецификацией HTTP сам этот новый ресурс должен быть расположен по URL-адресу. В частности, ваш подход нарушается в том, что URL-адрес, который вы используете с PUT, является представлением транзакционного контекста, созданного POST, а не представлением самого нового ресурса. Другими словами, запись о человеке — это побочный эффект обновления записи о транзакции, а не его непосредственный результат (обновленная запись о транзакции).

Конечно, при таком подходе запрос PUT будет идемпотентным, поскольку после создания записи человека и «завершения» транзакции последующие запросы PUT ничего не сделают. Но теперь у вас другая проблема — чтобы фактически обновить эту запись о человеке, вам нужно будет сделать запрос PUT к другому URL-адресу — тому, который представляет запись о человеке, а не транзакцию, в которой она была создана. Итак, теперь у вас есть два отдельных URL-адреса, которые ваши API-клиенты должны знать и отправлять запросы для управления одним и тем же ресурсом.

Или вы можете иметь полное представление о последнем состоянии ресурсов, скопированное в запись транзакции, а обновления записи о человеке также проходить через URL-адрес транзакции для обновлений. Но на данный момент URL-адрес транзакции является по назначению и целям записи человека, что означает, что он был создан в первую очередь по запросу POST.

person Franci Penov    schedule 02.06.2010
comment
Если вы хотите, чтобы имя ресурса больше походило на существительное, вы можете переименовать его в CREATE_PERSON_TRANSACTION. Запрос: PUT CREATE_PERSON_TRANSACTION/{transaction_id} не нарушает правила, согласно которому ресурс должен быть сохранен по этому URL-адресу, потому что: GET CREATE_PERSON_TRANSACTION/{transaction_id} вернет информацию о транзакции, например, выполняется ли транзакция или завершена. Это не вернет человека. - person Chris Dutrow; 02.06.2010
comment
Использование POST PERSONS/ для создания нового ресурса человека не является идемпотентным. Идемпотент означает, что запрос может быть выполнен один или несколько раз, и результат будет одинаковым. Это важно, поскольку идемпотентный запрос может быть повторно отправлен клиентом, если клиент не получил ответа. Причина, по которой я разместил этот вопрос, заключалась в том, чтобы получить представление о создании ресурсов идемпотентно. - person Chris Dutrow; 02.06.2010
comment
@Franci Я согласен, что выбранные имена ресурсов сбивают с толку, но я не согласен с тем, что операции, которые он выполняет, являются проблемой. Просто измените имена на POST /PersonIdentifiers PUT/Person/{Identifier} Я не вижу проблем с созданием идентификатора в качестве отдельного шага. Пока никогда не создаются два одинаковых идентификатора и нет требования, чтобы каждый идентификатор действительно использовался для создания человека, мне кажется, это нормально. - person Darrel Miller; 02.06.2010
comment
@Darrel - это то, что я пытался заставить его сделать. Однако он хочет создать отдельный идентификатор транзакции с запросом POST и создать запись человека с PUT для URL-адреса для транзакции и получить фактический идентификатор человека. - person Franci Penov; 03.06.2010
comment
@Franci Хорошо, я пропустил этот PersonKey. Да, согласен, это странно. Весь смысл единого интерфейса в том, что вы делаете то, чего ожидают люди, а не просто соблюдаете букву закона. - person Darrel Miller; 03.06.2010
comment
@Franci - я только что увидел твое обновление. Думаю, вы правы, подход, который я предлагаю, слишком сумбурный. Я рассматриваю возможность просто позволить клиенту создать GUID в качестве ключа к ресурсу. Таким образом, чтобы создать или обновить ресурс, они могут просто выполнить PUT PERSON/{GUID}. Затем ПОЛУЧИТЕ PERSON/{GUID}, чтобы прочитать его. Раньше я не рассматривал возможность того, что клиент мог продиктовать ключ ресурса. Большое спасибо за ваш отзыв по этому вопросу. - person Chris Dutrow; 06.06.2010
comment
-1 Я думаю, что этот ответ полностью упускает из виду тот факт, что если вы отправляете что-то без идентификатора запроса, нет способа обработать потерянные сообщения на сервере и, следовательно, способ справиться с идемпотентностью. - person Henrik; 25.04.2012
comment
@ Хенрик - а?! POST по определению не идемпотент. Два идентичных POST должны привести к созданию двух новых подчиненных ресурсов. Согласно HTTP. Нет потерянных сообщений, если вы POST' and the new resource was created successfully, the response should contain the Location` заголовок с URI для нового ресурса. - person Franci Penov; 26.04.2012
comment
Но вопрос конкретно в том, что происходит, когда вы учитываете реальные сети в уравнении — я мог бы представить добавление заголовка к запросу POST с идентификатором запроса, чтобы сервер мог выполнять дедупликацию. - person Henrik; 26.04.2012
comment
@Henrik - вопрос в том, как избежать двух операций (данные GET id/PUT). HTTP уже предоставляет механизм — данные POST, и вы получите новый идентификатор в заголовке ответа Location. Дедупликация (без создания второй записи с теми же данными) является ортогональной и является тем, что бизнес-логика сервера должна выполнять в обоих случаях, поскольку она зависит исключительно от семантики данных. - person Franci Penov; 26.04.2012
comment
@FranciPenov Это не ортогонально - оно может быть частью запроса, тем самым избегая двух операций, но по-прежнему используя POST - но посмотрите на заголовок - это «идемпотентный REST», и хотя вы на 100% правы в отношении семантики из глаголов, не имеющих конкретного идентификатора запроса или подобного, вы на самом деле не отвечаете на вопрос об идемпотентности. - person Henrik; 26.04.2012
comment
@ Хенрик Ты технически прав. Как комментирует OP. Причина, по которой я разместил этот вопрос, заключалась в том, чтобы получить представление о создании ресурсов идемпотентно. Способы сделать это: 1) Используйте два запроса GET id/PUT data; 2) Создайте уникальный идентификатор на клиенте и выполните PUT данные; или 3) данные POST и ожидать, что сервер вернет ошибку, если POST приведет к дублированию. Ваше предложение представляет собой смесь 2) и 3) и будет работать, но оно слишком сложное, подвержено ошибкам и функционально эквивалентно только 3). - person Franci Penov; 26.04.2012
comment
@ Хенрик ... но если вы выберете только 3), теперь решение именно то, на что я ответил, а дедупликация основана на семантическом анализе данных и полностью ортогональна доставке данных на сервер. - person Franci Penov; 26.04.2012
comment
@Henrik ... кстати, идея о том, что клиент может помочь дедупликации, добавив идентификатор в POST, предполагает, что клиент хорошо знает, что сервер считает дубликатами данных, что в случае общедоступного REST API не является всегда так. - person Franci Penov; 26.04.2012
comment
@FranciPenov это не сложно; проблема в том, что клиент не может получить доступ к своему MAC-адресу, поэтому ему придется основывать свою уникальность для идентификатора запроса на чем-то на стороне сервера или на чем-то вроде строки пользовательского агента + галочки: идентификаторы сообщений являются обычными и довольно хорошо известны , поэтому я предлагаю сначала GET /msgids, а затем POST /resource с заголовком X-Request-Id: 56020ef. Это называется внеполосными данными; и поскольку это так, на самом деле это не часть бизнес-данных и, следовательно, не часть чего-то однозначно идентифицируемого на сервере (а не на клиенте)... - person Henrik; 27.04.2012
comment
@FranciPenov Это сказало; Я согласен с тем, что публикация дважды будет в порядке; если был способ получить Location на второй ответ - в таком случае; какой HTTP-код вы бы вернули? - person Henrik; 27.04.2012
comment
@ Хенрик HTTP 303 See Other, в котором говорится: «Пожалуйста, выполните GET по этому другому URL-адресу вместо текущего запроса». Заголовок Location будет содержать URL-адрес дедуплицированного ресурса. - person Franci Penov; 27.04.2012

Я только что наткнулся на этот пост: Простое доказательство того, что GUID не уникален

Хотя этот вопрос повсеместно высмеивают, некоторые ответы содержат более глубокое объяснение GUID. Похоже, что GUID представляет собой число размером 2 ^ 128, и вероятность случайного создания двух одинаковых чисел такого размера настолько мала, что практически невозможна.

Возможно, клиент мог бы просто сгенерировать свой собственный идентификатор транзакции размером с GUID вместо того, чтобы запрашивать его у сервера. Если кто-то может дискредитировать это, пожалуйста, дайте мне знать.

person Chris Dutrow    schedule 05.06.2010
comment
Было бы абсолютно правильно создать GUID на клиенте, а затем выполнить PUT /Person/{Guid}. Однако я действительно не понимаю, для чего это понятие идентификатора транзакции. - person Darrel Miller; 05.06.2010
comment
Возможно, в этом случае лучше использовать термин request_id. Идея состоит в том, чтобы клиент мог снова сделать тот же запрос, если он не получил ответа от сервера в первый раз, и быть уверенным в идемпотентности. Запрос будет идемпотентным, потому что сервер может посмотреть на request_id и, если он соответствует уже сделанному запросу, он отправит ответ, указывающий, что запрос является дубликатом, вместо того, чтобы снова обрабатывать запрос и добавлять дубликата в базу данных. - person Chris Dutrow; 05.06.2010
comment
@ChrisDutrow, я думаю, что ваш подход уместен и, кажется, соответствует замыслу дизайна PUT. - person Mahmoud Abdelkader; 19.05.2013

Я не уверен, что у меня есть прямой ответ на ваш вопрос, но я вижу несколько вопросов, которые могут привести к ответам.

Ваша первая операция — это GET, но это небезопасная операция, поскольку она «создает» новый идентификатор транзакции. Я бы предположил, что POST является более подходящим глаголом для использования.

Вы упомянули, что вас беспокоят проблемы с производительностью, которые могут быть восприняты пользователем из-за двух круговых поездок. Это потому, что ваш пользователь собирается создать 500 объектов одновременно, или потому что вы находитесь в сети с огромными задержками?

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

person Darrel Miller    schedule 02.06.2010
comment
Да, вы правы, это должен быть POST, я его изменил. Ваш ответ может подчеркивать мою неопытность в этой архитектуре. Раньше я создавал это приложение с помощью ASP.NET, и оно очень, очень медленное. - person Chris Dutrow; 02.06.2010
comment
@DutrowLLC Я вполне уверен, что ваше приложение не работает медленно, потому что вы совершаете два обхода, когда пользователь создает объект. - person Darrel Miller; 05.06.2010
comment
Да, ASP.NET плохо подходил для проекта и, казалось, имел очень высокие накладные расходы, которые все замедляли. Кроме того, я использовал Entity Framework, который также может быть очень неэффективным. - person Chris Dutrow; 05.06.2010

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


POST /persons

first_name=foo

ответ будет:


HTTP 201 CREATED
...
payload_containing_data_and_auto_generated_id

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

person manuel aldana    schedule 02.06.2010
comment
Это правильный способ сделать это, но этот запрос POST не является идемпотентным запросом, который, похоже, вызывает ошибку в OP. - person Franci Penov; 03.06.2010
comment
а, понятно... теперь вопрос в том, почему он должен быть идемпотентным? - person manuel aldana; 03.06.2010
comment
@manuelaldana должен быть идемпотентным, потому что он хочет создать только одного человека, а не двух. Поэтому, если первый ответ потерян, клиент не может сопоставить свой следующий запрос, чтобы получить сведения о ресурсе. - person Henrik; 25.04.2012