RESTFul: действия, изменяющие состояние

60

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

Допустим, у нас есть статья в качестве ресурса (api / article), как мы должны реализовывать такие действия, как публикация, отмена публикации, активация или деактивация и т. Д., Но при этом стараться сделать ее максимально простой?

1) Должны ли мы использовать api / article / {id} / {action}, так как там может происходить много бэкэнд-логики, например, перемещение в удаленные местоположения или изменение нескольких свойств. Вероятно, самое сложное здесь заключается в том, что нам нужно отправить все данные статьи обратно в API для обновления, и многопользовательская работа не может быть реализована. Например, редактор может отправить данные на 5 секунд старше и исправить перезапись, что некоторые другие журналисты только что сделали 2 секунды назад, и я никак не мог объяснить это клиентам, поскольку те, кто публикует статью, на самом деле никак не связаны с обновлением контента.

2) Создание нового ресурса также может быть опцией api / article- {action} / id, но тогда возвращаемым ресурсом будет не article-{action}, а article, в котором я не уверен, правильно ли это. Кроме того, в статье, посвященной классу кода на стороне сервера, выполняется актуальная работа с обоими ресурсами, и я не уверен, противоречит ли это RESTfull.

Любые предложения приветствуются ..

Миро Свртан
источник
Совершенно законно, чтобы «действия» были частью RESTful URI - если они указывают действие / алгоритм, который нужно выполнить. Что не так с api/article?action=publish? Параметры запроса предназначены для тех случаев, когда состояние ресурса зависит от упомянутого вами «алгоритма» (или действия). Например api/articles?sort=asc, действительный
доктор философии
1
Я предлагаю вам ознакомиться с этой статьей, которая может вдохновить вас на еще более RESTful решение.
Бенджол
Одна из проблем, с которыми я сталкиваюсь при использовании api / article? Action = publish, заключается в том, что в приложении RESTfull следует отправлять ВСЕ данные статьи для публикации, а я бы предпочел сделать это: api / article / 4545 / publish / без каких-либо дополнительных
действий
2
Отличный ресурс по этому вопросу и многое другое: REST API Design - Моделирование ресурсов
Изаки
@PhD: хотя в протоколе это совершенно законно, URI, как правило, должны быть существительными, а не глаголами. Наличие глагола в URI обычно является признаком плохого дизайна REST.
Ли Райан

Ответы:

49

Я считаю полезными описанные здесь методы :

А как насчет действий, которые не вписываются в мир операций CRUD?

Это где вещи могут стать нечеткими. Есть несколько подходов:

  1. Перестройте действие, чтобы оно выглядело как поле ресурса. Это работает, если действие не принимает параметры. Например, действие активации может быть сопоставлено с логическим activatedполем и обновлено через PATCH для ресурса.
  2. Рассматривайте это как подресурс с принципами RESTful. Например, API GitHub позволяет вам звезду Сущности с PUT /gists/:id/starи снять пометку с DELETE /gists/:id/star.
  3. Иногда у вас действительно нет возможности сопоставить действие с разумной структурой RESTful. Например, поиск по нескольким ресурсам не имеет смысла применять к конечной точке конкретного ресурса. В этом случае, /searchбудет иметь смысл, даже если это не ресурс. Это нормально - просто делайте то, что правильно с точки зрения потребителя API, и убедитесь, что он задокументирован четко, чтобы избежать путаницы.
Тим
источник
Я голосую за подход 2. Хотя это может выглядеть как неуклюжие искусственные ресурсы для вызывающих API, в действительности они не знают, что происходит на сервере. Если я вызову POST /article/123/deactivationsдля создания нового запроса на деактивацию для статьи 123, сервер может не только деактивировать запрошенный ресурс, но и фактически сохранить мой запрос на деактивацию, чтобы я мог получить его статус позже.
JustAMartin
2
почему PUT /gists/:id/star нет POST /gists/:id/star?
Филипп Бартузи
11
@FilipBartuzi Поскольку PUT является идемпотентом - то есть, независимо от того, сколько раз вы выполняете действие с одинаковыми параметрами, результат всегда одинаков (например, если вы включаете свет, он меняется. Если вы пытаетесь включить это снова, ничего не меняется - это уже включено). POST не идемпотентен - то есть каждый раз, когда вы выполняете действие, даже с теми же параметрами, действие имеет другой результат (например, если вы отправляете письмо кому-то, это лицо получает письмо. Если вы отправляете идентичное письмо тот же человек, у них теперь 2 буквы).
Рафаэль
9

Операции, приводящие к серьезным изменениям состояния и поведения на стороне сервера, такие как описанное вами действие «публикация», сложно моделировать в REST. Решение, которое я часто вижу, состоит в том, чтобы неявно управлять таким сложным поведением через данные.

Рассмотрите возможность заказа товаров через REST API, предоставляемый онлайн-продавцом. Заказ - сложная операция. Несколько продуктов будут упакованы и отправлены, с вашего счета будет снята оплата, и вы получите квитанцию. Вы можете отменить свой заказ в течение ограниченного периода времени, и, конечно, есть полная гарантия возврата денег, которая позволяет вам вернуть продукты для возврата.

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

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

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

Ференц Михай
источник
Идея изменить весь ресурс с неопубликованной статьи на черновой вариант подходит именно для этого случая, но не подходит для всех других действий, которые существуют на ресурсе в целом. REST даже должен справиться с этим? Может быть, я злоупотребляю им и должен использовать его только как CRUD и ничего более, например, запросы SQL, где я не ожидаю, что какая-либо логика будет внутри самого запроса.
Миро Свртан
Почему я все это спрашиваю? Ну, так как некоторое время назад веб-приложения стали мультиплатформенными, и я бы предпочел сохранить большую логику бизнеса на сервере, поскольку обновление бизнес-логики на iOS, Android, в Интернете, на компьютере или любой другой платформе становится практически невозможным быстро, и я хотел бы избежать всех проблем обратной совместимости при замене небольшого фрагмента BL.
Миро Свртан
2
Я думаю, что REST прекрасно справляется с бизнес-логикой, но он не подходит для демонстрации существующей бизнес-логики, написанной без учета REST. Вот почему многие компании, такие как Microsoft, SAP и другие, часто представляют данные только с помощью операций CRUD, как вы сказали. Взгляните на протокол открытых данных, чтобы увидеть, как они это делают.
Ференц Михай
7

нам нужно отправить все данные статьи обратно в API для обновления, и многопользовательская работа не может быть реализована. Например, редактор может отправить данные на 5 секунд старше и исправить перезапись, что некоторые другие журналисты только что сделали 2 секунды назад, и я никак не мог объяснить это клиентам, поскольку те, кто публикует статью, на самом деле никак не связаны с обновлением контента.

Подобные вещи являются проблемой, независимо от того, что вы делаете, это очень похоже на проблему с распределенным управлением исходным кодом (Mercurial, Git и т. Д.), И решение, записанное в HTTP / ReST, выглядит немного похожим.

Предположим, у вас есть два пользователя, Алиса и Боб, оба работают над ними /articles/lunch. (для ясности ответ выделен жирным шрифтом)

Во-первых, Алиса создает статью.

PUT /articles/lunch HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

301 Moved Permanently
Location: /articles/lunch/1 

Сервер не создал ресурс, потому что к запросу не было прикреплено никакой «версии» (при условии, что /articles/{id}/{version}для выполнения создания был создан идентификатор . Алиса была перенаправлена ​​на URL-адрес статьи / версии, которую она будет создавать. Пользователь Алисы). Затем агент повторно применяет запрос по новому адресу.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?

201 Created

И теперь статья была создана. Далее Боб смотрит на статью:

GET /articles/lunch HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

301 Moved Permanently
Location: /articles/lunch/1 

Боб смотрит туда:

GET /articles/lunch/1 HTTP/1.1
Host: example.com
Authorization: Basic Ym9iOnBhc3N3b3Jk

200 Ok
Content-Type: text/plain

Hey Bob, what do you want for lunch today?

Он решает добавить свои собственные изменения.

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

301 Moved Permanently
Location: /articles/lunch/2

Как и в случае с Алисой, Боб перенаправлен туда, где он будет создавать новую версию.

PUT /articles/lunch/2 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic Ym9iOnBhc3N3b3Jk

Hey Bob, what do you want for lunch today?
Does pizza sound good to you, Alice?

201 Created

Наконец, Алиса решает, что она хотела бы добавить к своей собственной статье:

PUT /articles/lunch/1 HTTP/1.1
Host: example.com
Content-Type: text/plain
Authorization: Basic YWxpY2U6c2VjcmV0

Hey Bob, what do you want for lunch today?
I was thinking about getting Sushi.

409 Conflict
Location: /articles/lunch/3
Content-Type: text/diff

---/articles/lunch/2
+++/articles/lunch/3
@@ 1,2 1,2 @@
 Hey Bob, what do you want for lunch today?
-Does pizza sound good to you, Alice?
+I was thinking about getting Sushi.

Вместо перенаправления в обычном режиме клиенту возвращается другой код состояния 409, который сообщает Алисе, что версия, из которой она пыталась перейти, уже разветвлена. Новые ресурсы были созданы в любом случае (как показано в Locationзаголовке), и различия между ними были включены в тело ответа. Алиса теперь знает, что запрос, который она только что сделала, нужно как-то объединить.


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

SingleNegationElimination
источник
1
Versoning не был проблемой здесь, я просто сказал это как пример возможных проблем, если использовать статью в качестве ресурса для публикации действий
Miro Svrtan
3

Вот еще один пример, который касается не содержимого документов, а скорее переходного состояния. (Я нахожу управление версиями - учитывая, что, как правило, каждая версия может быть новым ресурсом - своего рода легкая проблема.)

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

Какой подход RESTful здесь? POST / service? Команда = перезагрузка, например? Или POST / сервис / состояние с телом, скажем, «работает»?

Здесь было бы неплохо систематизировать лучшие практики и определить, является ли REST правильным подходом к подобным ситуациям.

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

GET / report может быть способом получения копии отчета самостоятельно; но что, если мы хотим подтолкнуть на сторону сервера дальнейшие действия, такие как отправка электронной почты, как я сказал выше. Или запись в базу данных.

Эти случаи танцуют вокруг разрыва между ресурсами и действиями, и я вижу способы обращаться с ними REST-ориентированным способом, но, честно говоря, это похоже на хакерство. Возможно, ключевой вопрос заключается в том, должен ли REST API поддерживать побочные эффекты в целом.

AMD
источник
2

REST ориентирован на данные, и такие ресурсы лучше всего работают как «вещи», а не действия. Неявная семантика http-методов; GET, PUT, DELETE и т. Д. Служат для усиления ориентации. POST, конечно, является исключением.

Ресурс может быть смесью данных, т.е. содержание статьи; и метаданные т.е. опубликовано, заблокировано, редакция. Есть много других возможных способов срезать данные, но сначала вам нужно будет посмотреть, как будет выглядеть поток данных, чтобы определить наиболее оптимальный (если он есть). Например, может случиться так, что редакции должны быть их собственным ресурсом согласно статье, как предлагает TokenMacGuy.

Что касается реализации, я бы, вероятно, сделал что-то вроде того, что предлагает TockenMacGuy. Я бы также добавил поля метаданных в статью, а не ревизию, например «заблокирован» и «опубликован».

dietbuddha
источник
1

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

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

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

Ключевым моментом для меня было признание, что метафора порядка изменений - это просто еще один способ описания объектно-ориентированного программирования. Вместо ресурсов мы называем объекты. Вместо того, чтобы менять порядок, мы называем их сообщениями. Один из способов отправить сообщение от A к B в OO - это вызвать A для вызова метода на B. Другой способ сделать это, особенно когда A и B находятся на разных компьютерах, это заставить A создать новый объект, M и отправить его Б. ОТДЫХ просто формализует этот процесс.

Патрик МакЭлхани
источник
На самом деле я бы посчитал это ближе к модели актера, чем к ОО. Рассматривая REST-запросы как сообщения (которые они есть), они очень аккуратно выстраиваются в соответствии с источником событий для управления состоянием. REST аккуратно делит свои взаимодействия вдоль линий CQRS. GET-сообщения - это запросы, POST, PUT, PATCH, DELETE, попадающие в область команд.
WillD
0

Если я вас правильно понимаю, я думаю, что то, что у вас есть, является скорее вопросом определения «бизнес-правила», чем технической проблемой.

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

Во всех вышеперечисленных случаях вашей службе необходимо реализовать установленные вами бизнес-правила. Таким образом, вы можете вызвать службу с параметрами: идентификатор пользователя, статья, версия, действие (если версия является необязательной, опять же, это зависит от ваших бизнес-правил).

Без шансов
источник
Я не верю, что это правило бизнеса, но строго техническое. Идея добавления версии хороша, чтобы помочь с переопределением правил, но все еще не решает ситуацию, когда обновление контента и публикация контента не являются связанными действиями.
Миро Свртан