RESTful идемпотентность - PullRequest
5 голосов
/ 02 июня 2010

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

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

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

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

**Client request:**
POST /CREATE_PERSON

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

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

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

Ответы [ 4 ]

4 голосов
/ 02 июня 2010

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

Пункт 9.5:

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

Пункт 9,6

PUT метод требует, чтобы закрытая сущность должна храниться под предоставлен 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 на первом месте.

1 голос
/ 05 июня 2010

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

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

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

1 голос
/ 02 июня 2010

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

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

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

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

0 голосов
/ 03 июня 2010

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


POST /persons

first_name=foo

ответ будет:


HTTP 201 CREATED
...
payload_containing_data_and_auto_generated_id

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...